UNIX网络编程——非阻塞connect: Web客户程序
非阻塞的connect的实现例子出自Netscape的Web客户程序。客户先建立一个与某个Web服务器的HTTP连接,再获取一个主页。该主页往往含有多个对于其他网页的引用。客户可以使用非阻塞connect同时获取多个网页,以此取代每次只获取一个网页的串行获取手段。图16-12展示了一个并行建立多个连接的例子。最左边情形表示串行执行所有3个连接。假设第一个连接耗用10个时间单位,第二个耗时15个,第三个耗用4个,总机29个时间单位。
中间情形并行执行2个连接。在时刻0启动前2个连接,当其中之一结束时,启动第三个连接。总计耗时差不多减半,从29变为15,不过必须意识到这是就理想情况而言的。如果并行执行的连接共享一个低俗链路,那么每个连接可能彼此竞用邮箱的资源,使得每个连接可能耗用更长的时间。举例来说,10个时间单位的连接可能变为15,15个时间单位的可能变为20,4个时间单位的可能变为6.即使如此,总计耗时将是21,仍然短于串行执行的情形。
最右边情形并行执行所有3个连接,其中再次假设这3个连接之间没有干扰(理想情况)。然而就我们选择的例子时间而言,本情况的总计耗时和中间情形的一样,都是15个时间单位。
在处理Web客户时,第一个连接独立执行,来自连接的数据含有多个引用,然后用于访问这些引用的多个连接则并行执行,如图16-13所示。
为了进一步优化连接执行序列,客户可以在第一个连接尚未完成前就开始从中陆续返回的数据,以便尽早得悉其中含有的引用,并尽快启动相应的额外连接。
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_CONNECTING 1 /* connect() in progress */ #define F_READING 2 /* connect() complete; now 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 *);
定义file结构
03-11 本程序最多读MAXFILES个来自Web服务器的文件。我们维护一个file结构,其中包含关于每个文件的信息:文件名(复制自命令行参数),文件所在服务器主机名或IP地址,用于读取文件的套接字描述符以及用于指定准备对文件执行什么操作(连接,读取或完成)的一组标志。
定义全局变量和函数原型
20-26 定义全局变量和稍后讲解的各个函数的函数原型
Web.c
#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); 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]); FD_ZERO(&rset); FD_ZERO(&wset); maxfd = -1; nlefttoread = nlefttoconn = nfiles; nconn = 0; while (nlefttoread > 0) { while (nconn < maxnconn && nlefttoconn > 0) { /* 4find 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); } /* 4connection 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); }
处理命令行参数
14-20 以来自命令行参数的相关信息填写file结构数组。
读取主页
22 接着给出的home_page函数创建一个TCP连接,发出一个命令道服务器,然后读取主页。这是第一个连接,需在我们开始并行建立多个连接之前独自完成。
初始化全局变量
24-28 初始化两个描述符,一个用于读一个用于写。maxfd是select需要的最大描述符(我们把它初始化成-1,因为描述符都是非负的),nlefttoread是仍待读取得文件数(当它到达0时程序任务完成),nlefttoconn是尚无TCP连接的文件数,nconn是当前打开着的连接数(它不能超过第一个命令行参数)。
可能的话发起另一个连接
29-40 如果没有到达最大并行连接数而且另有连接需要建立,那就找到一个尚未处理的文件(由值0的f_flags指示),然后调用start_connect发起另一个连接。活跃连接数(nconn)增1,仍待建立连接数(nlefttoconn)减1。
select:等待事件发生
42-43 select等待的不是可读条件就是可写条件。有一个非阻塞connect正在进展的描述符可能会同时开启这两个描述符集,而连接建立完毕并正在等待来自服务器的数据的描述符只会开启读描述符集。
处理所有就绪的描述符
46-64 遍查file结构数组中的每个元素,确定哪些描述符需要处理。对于数组了F_CONNECT标志的一个描述符,如果它在读描述符集或写描述符集中对于的位已打开,那么非阻connect已经完成。我们调用getsockopt获取该套接字的待处理错误。如果该值为0,那么连接已经成功建立。这种情况下我们关闭该描述符在写描述符集中对应的位,然后调用write_get_cmd发送HTTP请求到服务器。
检查描述符是否有数据
65-77 对于数组了F_READING标志的一个描述符,如果它在读描述符集中对应的位已打开,我们调用read。如果相应连接被对端关闭,我们就关闭该套接字,并设置F_DONE标志,然后关闭该描述符在读描述符集中对应的位,把活动连接数和要处理的连接总数都减1.
home_page.c
#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); 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); }
建立与服务器的连接
9 我们的tcp_connect会建立一个与服务器的连接。
发送HTTP命令道服务器,读取应答
11-22 发出一个HTTP GET命令以获取主页(文件名经常是/)。读取应答(我们不对应答做任何操作),然后关闭连接。
start_connect
#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); fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); fptr->f_fd = fd; printf("start_connect for %s, fd %d\n", fptr->f_name, fd); /* 4Set socket nonblocking */ flags = Fcntl(fd, F_GETFL, 0); Fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 4Initiate nonblocking connect to the server. */ if ( (n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0) { 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; } else if (n >= 0) /* connect is already done */ write_get_cmd(fptr); /* write() the GET command */ }
创建套接字,设置为非阻塞
9-17 调用我们的host_serv函数,查找并转换主机名和服务器名,它返回指向某个addrinfo结构数组的一个指针。我们只使用其中第一个结构。创建一个TCP套接字并把它设置为非阻塞。
发起非阻塞
20-27 发起非阻塞connect,并把相应文件的标志设置为F_CONNECTING。在读描述符集和写描述符集中对应的位打开套接字描述符,因为select将等待其中任何一个条件变为真,作为连接已建立完毕的指示。我们还根据需要更新maxfd的值。
处理连接建立完成情况
29-30 如果connect成功返回,那么连接已经建立,于是调用write_get_cmd函数(接着给出)发送一个命令到服务器。
我们为connect把套接字设置为非阻塞后,不再把它重置为默认的阻塞模式。这么做没有问题,因为我们只往套接字中写出少量的数据(下一个函数中的GET命令,可以认为它比套接字发送缓冲区小得多)。即使write因为非阻塞标志造成返回一个不足计数,我们的writen函数也会对此进行处理。套接字继续处于非阻塞模式对于后续的read也没有影响,因为我们总是在调用select等待套接字变为可读后才调用read。
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; }
构造命令并发送
9-11 构造命令并写出到套接字。
设置标志
13-17 设置相应文件的F_READING标志,它同时清除F_CONNECTING(如果设置了的话)。该标志向main函数主循环指出,本描述符已经准备好提供输入。在读描述符集中打开与本描述符对应的位,并根据需要更新maxfd。