在TCP 客户/服务器程序示例这一节中,我们编写了一个多进程的TCP回显服务器程序,本节我们使用select函数监听服务器套接字和所有的客户的套接字来实现同样的功能。具体实现就是每当一个新的客户建立连接,我们把它的套接字描述符(为了与服务器程序自身的监听描述符作区分,我称服务器程序为服务客户创建的套接字为“客户套接字”)保存在一个数组中,然后将该描述符加入select函数监听的读描述符集中,然后循环处理所有的读事件。具体代码如下:
#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;
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /*将服务器套接字描述符加入读描述符集中*/
for ( ; ; ) {
rset = allset;
nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { /*新的客户连接*/
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
/*找到第一个未使用的项,存入客户套接字描述符*/
for (i = 0; i < FD_SETSIZE; i++) {
if (client[i] < 0) {
client[i] = connfd;
break;
}
}
if (i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
if (i > maxi)
maxi = i;
if (--nready <= 0)
continue;
}
for (i = 0; i <= maxi; i++) { /*轮询所有的客户套接字描述符*/
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) { /*该客户套接字有待读取的数据*/
if ((n = Read(sockfd, buf, MAXLINE)) == 0) { /*客户关闭连接*/
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break;
}
}
}
}
书中说这段代码有拒绝服务的缺陷,是说服务器调用read函数阻塞于某一个用户的套接字上,不能服务其他的客户。我实际试验了下没有发现有这个缺陷,也看不出来代码哪里有拒绝服务的缺陷。
这段代码有一个缺陷就是处理客户的套接字时没有考虑异常情况。比如说客户异常终止,给服务器发送RST,那么代码里的Read(注意是包裹函数)函数会返回“read error: Connection reset by peer”,服务器进程会终止。下节使用poll函数实现的版本会修正这个缺陷。