为了可以处理多个客户的请求,我们之前一直使用多进程TCP并发服务器,socket()
监听一个套接口,accept()
多个用户,父进程监听listenfd
,子线程们在connfd
上进行应答处理。
通过使用select
函数,我们可以在单进程服务器的前提下,处理多客户的请求,而无需为每个客户派生一个子进程。下面描述此模型下的处于不同阶段的服务器状态。
在还没有客户建立连接时,服务器有单个监听描述字。
服务器只维护一个读描述字集。在终端启动服务器,则描述字0、1和2分别为标准输入,标准输出和标准错误输出,因此分给监听套接口的第一个可用描述字是3。
fd 0 | fd 1 | fd 2 | fd 3 |
---|---|---|---|
0 | 0 | 0 | 1 |
使用一个整型数组维护客户的已连接描述字,在还没有客户建立连接前,数组中的元素都为初始值-1。
connfd 0 | connfd 1 | connfd 2 | ... | connfd FD_SETSIZE-1 |
---|---|---|---|---|
-1 | -1 | -1 | ... | -1 |
当第一个客户与服务器建立连接时,监听描述字变为可读,服务器调用accept()
,分配给已连接套接口的描述字为4。
fd 0 | fd 1 | fd 2 | fd 3 | fd 4 |
---|---|---|---|---|
0 | 0 | 0 | 1 | 1 |
随后在数组中记录客户的已连接套接口描述字,client[0] = 4
。
connfd 0 | connfd 1 | connfd 2 | ... | connfd FD_SETSIZE-1 |
---|---|---|---|---|
4 | -1 | -1 | ... | -1 |
当第二个客户与服务器建立连接时,监听描述字变为可读,服务器调用accept()
,分配给已连接套接口的描述字为5。
fd 0 | fd 1 | fd 2 | fd 3 | fd 4 | fd 5 |
---|---|---|---|---|---|
0 | 0 | 0 | 1 | 1 | 1 |
随后在数组中记录第二个客户的已连接套接口描述字,client[1] = 5
。
connfd 0 | connfd 1 | connfd 2 | ... | connfd FD_SETSIZE-1 |
---|---|---|---|---|
4 | 5 | -1 | ... | -1 |
当第一个客户与服务器终止连接时,客户TCP发送一个FIN,服务器侧描述字4变得可读,读此已连接套接口时,
readline返回0。接着关闭此已连接套接口并更新数据结构,
client0 = -1`
fd 0 | fd 1 | fd 2 | fd 3 | fd 4 | fd 5 |
---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 1 |
随后在数组中记录第二个客户的已连接套接口描述字,client[1] = 5
。
connfd 0 | connfd 1 | connfd 2 | ... | connfd FD_SETSIZE-1 |
---|---|---|---|---|
-1 | 5 | -1 | ... | -1 |
#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;
同样,依次调用socket
,bind
,listen
创建监听套接口。一开始select
的唯一描述字便是监听套接口描述字。
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; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;) {
rset = allset; /* structure assignment */
nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);
如果监听套接口变为可读,则代表可建立新的连接。记录新的已连接套接口描述字,更新数据结构,直到没有更多的可读描述字。
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE) err_quit("too many clients");
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd) maxfd = connfd; /* for select */
if (i > maxi) maxi = i; /* max index in client[] array */
if (--nready <= 0) continue; /* no more readable descriptors */
}
对所有已连接的客户,测试数据是否准备好被读,若是,则回射一行给客户,若客户终止连接,那么相应地要更新数据结构。
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ((sockfd = client[i]) < 0) continue;
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0) break; /* no more readable descriptors */
}
}
编译运行服务器程序,
[root@VM_0_6_centos tcpcliserv]# make tcpservselect01
gcc -I../lib -g -O2 -D_REENTRANT -Wall -c -o tcpservselect01.o tcpservselect01.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpservselect01 tcpservselect01.o ../libunp.a -lpthread
[root@VM_0_6_centos tcpcliserv]# ./tcpservselect01
可以看到目前主机上有一个服务器进程
[root@VM_0_6_centos ~]# ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 0 30506 30352 0 80 0 - 1595 poll_s pts/0 00:00:00 tcpservselect01
0 R 0 30567 30523 0 80 0 - 38300 - pts/1 00:00:00 ps
在本地终端启动客户端,输入一行文本
jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 150.*.*.*
hello
hello
可以看到此时仍然只有一个进程。
[root@VM_0_6_centos ~]# ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 0 3941 3908 0 80 0 - 38292 - pts/2 00:00:00 ps
0 S 0 30506 30352 0 80 0 - 1595 poll_s pts/0 00:00:00 tcpservselect01
这个服务器程序较为复杂,但是它避免了为每个连接的客户创建一个新的进程,是select
的一个经典应用。
但是,这个服务器程序有一个问题。若有恶意客户连接到服务器上,发送单个字节而非一行之后睡眠。服务器会调用readline
,它读完该客户的一个字节,然后就阻塞于下一个read
以等待这个客户的其他数据,无法为其他客户提供服务。这种行为被称为拒绝服务型攻击。可能的解决方法是:
a) 使用非阻塞I/O模型
b) 为每个客户开一个单独的线程
c) 对I/O操作设置超时
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。