许多初学者都是从阻塞式IO网络编程开始的。如果一个IO操作是同步的,意味着当你调用相关的函数时,除非IO操作已经完成,否则函数不会立即返回,或者达到超时时间后才会返回。举例来说,当你使用TCP协议中的connect()函数时,你所在的操作系统将一个SYN包放入发往TCP另一端的数据队列中。除非从TCP另一端收到SYN ACK包,否则你的应用程序不会获得响应,或者经过足够的时间后放弃了连接网络才会获得响应。
这里有一个使用阻塞式IO的简单的客户端的例子:它打开一个www.google.com的连接,发送一个简单的HTTP请求,然后输出应答到stdout。
Example: A simple blocking HTTP client
/* For sockaddr_in */#include/* For socket functions */#include /* For gethostbyname */#include #include #include #include int main(int c, char **v){ const char query[] = "GET / HTTP/1.0\r\n" "Host: www.google.com\r\n" "\r\n"; const char hostname[] = "www.google.com"; struct sockaddr_in sin; struct hostent *h; const char *cp; int fd; ssize_t n_written, remaining; char buf[1024]; h = gethostbyname(hostname); if (!h) { fprintf(stderr, "Couldn't lookup %s: %s", hostname, hstrerror(h_errno)); return 1; } if (h->h_addrtype != AF_INET) { fprintf(stderr, "No ipv6 support, sorry."); return 1; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return 1; } /* Connect to the remote host. */ sin.sin_family = AF_INET; sin.sin_port = htons(80); sin.sin_addr = *(struct in_addr*)h->h_addr; if (connect(fd, (struct sockaddr*) &sin, sizeof(sin))) { perror("connect"); close(fd); return 1; } /* Write the query. */ /* XXX Can send succeed partially? */ cp = query; remaining = strlen(query); while (remaining) { n_written = send(fd, cp, remaining, 0); if (n_written <= 0) { perror("send"); return 1; } remaining -= n_written; cp += n_written; } /* Get an answer back. */ while (1) { ssize_t result = recv(fd, buf, sizeof(buf), 0); if (result == 0) { break; } else if (result < 0) { perror("recv"); close(fd); return 1; } fwrite(buf, 1, result, stdout); } close(fd); return 0;}
上面所有的网络函数调用都是阻塞的:gethostbyname在成功或者失败抵达www.google.com前不会返回;connect在连接成功之前不会返回;recv在获得数据或者关闭之前不会返回;send在刷新完输出数据到kernel' s write buffers之前不会返回。
即使到现在,阻塞式IO也不是毫无可取之处的。如果你的程序在IO时没有其它的事情要做,阻塞式IO是一个不错的选择。但是,假如你需要写一个能立即同时处理很多连接的程序,举例来说,假设你需要从2个连接读取输入数据,你是无法确定先从哪一个连接先读入的。因为假如第2个连接的数据先到达,你的程序就不会读取第2个连接的数据除非第1个连接的数据已经达到并且已经读取完毕。
有时候程序员们通过多线程的方式来解决这个问题,或者采用多进程的方式。这种方式的实现的最简单的方法之一就是:一个线程(进程)处理一个连接。因为每一个连接都有它自己的线程(进程),所以当一个连接阻塞了的时候并不会阻塞或者说影响到其它的连接线程(进程)的处理过程。
这里有另外一个例子,一个较为复杂的服务器。它监听40713端口,每次读取一行输入数据,并且writes out the ROT13 obfuscation of line each as it arrives。 它使用Unix的fork()函数为每一个新的连接创建一个新的进程。
Example: Forking ROT13 server
/* For sockaddr_in */#include/* For socket functions */#include #include #include #include #include #define MAX_LINE 16384charrot13_char(char c){ /* We don't want to use isalpha here; setting the locale would change * which characters are considered alphabetical. */ if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) return c + 13; else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) return c - 13; else return c;}void child(int fd){ char outbuf[MAX_LINE+1]; size_t outbuf_used = 0; ssize_t result; while (1) { char ch; result = recv(fd, &ch, 1, 0); if (result == 0) { break; } else if (result == -1) { perror("read"); break; } /* We do this test to keep the user from overflowing the buffer. */ if (outbuf_used < sizeof(outbuf)) { outbuf[outbuf_used++] = rot13_char(ch); } if (ch == '\n') { send(fd, outbuf, outbuf_used, 0); outbuf_used = 0; continue; } }}void run(void){ int listener; struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = 0; sin.sin_port = htons(40713); listener = socket(AF_INET, SOCK_STREAM, 0);#ifndef WIN32 { int one = 1; setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); }#endif if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) { perror("bind"); return; } if (listen(listener, 16)<0) { perror("listen"); return; } while (1) { struct sockaddr_storage ss; socklen_t slen = sizeof(ss); int fd = accept(listener, (struct sockaddr*)&ss, &slen); if (fd < 0) { perror("accept"); } else { if (fork() == 0) { child(fd); exit(0); } } }}int main(int c, char **v){ run(); return 0;}
至此,我们有了最完美的处理多个连接的解决方案了吗?我该停止继续写写下去做点别的事情吗?还没完。首先,创建进程(线程)在某些平台上会是一笔不小的开销。在实际实践中,你可能更想使用一个线程池来代替它。但是从根本上讲,多线程并不能够达到你所期望的那种扩展性。如果你程序需要同时处理成千上万个连接,对于每个CPU仅能尝试处理很少的线程的情况,处理成千上万个线程效率并不高。
如果多线程(进程)并不是解决多个连接的答案,那么该是什么呢?在Unix中,你可以使你的套接字变成非阻塞的。Unix系统下像这样 调用:
fcntl(fd, F_SETFL, O_NONBLOCK);
fd是套接字描述符。一旦你使套接字变成非阻塞的,从现在开始,不论你什么时候通过fd调用网络函数都会在操作完成后立即返回,或者立即返回一个特定的错误码来表明”我现在不能处理任何事情,请再次尝试“。因此,我们的2个连接的例子可以改写成这样:
Bad Example: busy-polling all sockets
/* This will work, but the performance will be unforgivably bad. */int i, n;char buf[1024];for (i=0; i < n_sockets; ++i) fcntl(fd[i], F_SETFL, O_NONBLOCK);while (i_still_want_to_read()) { for (i=0; i < n_sockets; ++i) { n = recv(fd[i], buf, sizeof(buf), 0); if (n == 0) { handle_close(fd[i]); } else if (n < 0) { if (errno == EAGAIN) ; /* The kernel didn't have any data for us to read. */ else handle_error(fd[i], errno); } else { handle_input(fd[i], buf, n); } }}
现在我们使用的是非阻塞套接字,上面的代码仅仅是能工作。它的性能极差,2个原因:第一,当没有数据可以读取时,循环体将会不停地无限循环,它将占用掉所有的CPU时间片;第2,在处理多个连接时,不管有没有数据,你都需要执行一次kernel call。因此,我们需要一个方法来让kenel做到:”一直等待这些套接字直到它有数据给我,并且告诉我是哪些套接字有数据了“。
这个问题的传统的解决方案是使用select()函数。select()能够管理三种类型事件的套接字集合(使用位数组实现的):一种是读取事件,一种是写入事件,一种是异常事件。它一直等待直到一个集合中的套接字就绪,并且通知集合中仅包含就绪的套接字。
下面是使用select的一个例子:
Example: Using select
/* If you only have a couple dozen fds, this version won't be awful */fd_set readset;int i, n;char buf[1024];while (i_still_want_to_read()) { int maxfd = -1; FD_ZERO(&readset); /* Add all of the interesting fds to readset */ for (i=0; i < n_sockets; ++i) { if (fd[i]>maxfd) maxfd = fd[i]; FD_SET(fd[i], &readset); } /* Wait until one or more fds are ready to read */ select(maxfd+1, &readset, NULL, NULL, NULL); /* Process all of the fds that are still set in readset */ for (i=0; i < n_sockets; ++i) { if (FD_ISSET(fd[i], &readset)) { n = recv(fd[i], buf, sizeof(buf), 0); if (n == 0) { handle_close(fd[i]); } else if (n < 0) { if (errno == EAGAIN) ; /* The kernel didn't have any data for us to read. */ else handle_error(fd[i], errno); } else { handle_input(fd[i], buf, n); } } }}