1. 使用select改写tcp服务器
在此之前,回顾一下多进程并发型服务器通信过程,并发型服务器的做法是针对每一个客户端请求,服务器的父进程就fork创建一个子进程来处理客户端的请求。如果有大量的客户端请求的话,那么服务器也需要创建大量的子进程来处理请求,这将会消耗服务器大量的系统资源。
因此我们可以使用select改写tcp服务器,通过select来处理多个客户端的请求,具体处理过程如下:
在客户端连接服务器之前,服务器只创建了单个文件描述符进行监听,在图1我们用一个方块表示。
服务器只维护一个读文件描述符集合rest,当服务器启动时描述符0,1,2分别表示标准输入,标准输出,标准出错,且这三个文件描述符被置为0,而服务器监听的描述符3被置为1,表示描述符3处于监听。而client数组中则记录每个客户端连接的描述符,开始时会将client数组初始化为-1,FD_SETSIZE代表服务器处理的最大客户端连接的数量,即client数组的大小。
在rest数组中,描述符3处于监听,因此select中的maxfd参数就是4。当第一个客户端与服务器建立tcp连接时,监听的描述符会变为可读,随后服务器调用accept,假设accept返回已连接的新描述符值是4,如图3所示:
那么在client数组中必须记录新的已连接描述符的值,同时把描述符4加入到rest集合中:
接着第二个客户端与服务器建立tcp连接:
同理,新的已连接描述符5也需要在client数组中记录,同时把描述符5加入到rest集合中去:
如果第一个客户端发送了FIN终止了tcp连接,那么服务器中的描述符4将会变的可读,当服务器读取描述符4将会返回0,于是可以关闭该套接字并更新rest集合将描述符4置为0,同时将client数组中client[0]的值置为-1,需要注意maxfd的值不变。
当有新的客户端建立tcp连接时,就可以在client数组中的第一项记录新的已连接描述符,并将新的已连接描述符添加到rest集合中。变量maxi是client数组中当前使用项的最大下标,变量maxfd(加1之后)则表示select函数的第一个参数的值。
2. 服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 1024
#define SERV_PORT 10001
int main(void)
{
int i, n ,maxi, maxfd, listenfd, connfd, sockfd;
int nready;
int client[FD_SETSIZE];
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
socklen_t cliaddr_len;
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, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
//初始化maxfd
maxfd = listenfd;
//client数组的下标
maxi = -1;
//初始化client数组
for (i = 0; i < FD_SETSIZE; i++){
client[i] = -1;
}
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select检测文件描述符集 */
while(1){
/* 每次循环时都重新设置select检测的描述符集合 */
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perror("select error:");
/* 判断是否有新的客户端连接建立完成 */
if (FD_ISSET(listenfd, &rset)) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
//在client数组中必须记录新的已连接描述符的值
for (i = 0; i < FD_SETSIZE; i++) {
if(client[i] < 0){
client[i] = connfd;
break;
}
}
/* 判断是否达到select能监控的文件个数上限 1024 */
if(i == FD_SETSIZE){
puts("too many clients");
exit(1);
}
/* 添加一个已连接的新描述符到监控信号集里监听 */
FD_SET(connfd, &allset);
/* 更新maxfd为最大描述符的值 */
if(connfd > maxfd)
maxfd = connfd;
/* 一旦i大于maxi的话,同时更新client数组中当前使用项的最大下标 */
if(i > maxi)
maxi = i;
/* 如果nready为0,说明I/O事件处理完毕 */
/* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未处理完的就绪文件描述符 */
if (--nready == 0)
continue;
}
/* 检测哪个clients有数据就绪 */
for (i = 0; i <= maxi; i++) {
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
//读取数据,有可能会读到0
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
/* 当client关闭链接时,服务器端也关闭对应链接 */
close(sockfd);
/* 取消select监听该文件描述符 */
FD_CLR(sockfd, &allset);
/* client数组中置为-1 */
client[i] = -1;
}else{
//读到数据后就处理数据
int j;
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(sockfd, buf, n);
}
/* 判断select返回的I/O事件是否处理完毕 */
if (--nready == 0)
break;
}
}
}
close(listenfd);
return 0;
}
程序执行结果: