TCP Server处理多Client请求的方法—非阻塞accept与select

参看基于TCP/UDP的socket代码,同一时间Server只能处理一个Client请求:在使用当前连接的socket和client进行交互的时候,不能够accept新的连接请求。为了使Server能够处理多个Client请求,常见的方法:

多进程/线程方法、non-blocking socket(单进程并发)、non-blocking和select结合使用。三种方法各有优缺点,下面进行详细分析和说明。

一、多进程/线程方法

这种方法,每个子进程/线程单独处理一个client连接。以使用进程为例,在每个accept成功之后,使用fork创建一个子进程专门处理该client的connection,父进程(server)本身可以继续accept其他新的client的连接请求。示例代码如下:

001#include <stdio.h>
002#include <stdlib.h>
003#include <string.h>
004#include <arpa/inet.h>
005#include <sys/types.h>
006#include <sys/socket.h>
007#include <unistd.h>
008 
009#include <signal.h>
010#include <sys/wait.h>
011 
012#define DEFAULT_PORT    1984    //默认端口
013#define BUFFER_SIZE     1024    //buffer大小
014 
015void sigCatcher(int n) {
016    //printf("a child process dies\n");
017    while(waitpid(-1, NULL, WNOHANG) > 0);
018}
019 
020int clientProcess(int new_sock);
021 
022int main(int argc, char *argv[]) {
023    unsignedshort int port;
024 
025    //get port, use default if not set
026    if(argc == 2) {
027        port =atoi(argv[1]);
028    } else if(argc < 2) {
029        port = DEFAULT_PORT;
030    } else {
031        fprintf(stderr,"USAGE: %s [port]\n", argv[0]);
032        return1;
033    }
034 
035    //create socket
036    intsock;
037    if( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
038        perror("socket failed, ");
039        return1;
040    }
041    printf("socket done\n");
042 
043    //create socket address and initialize
044    structsockaddr_in bind_addr;
045    memset(&bind_addr, 0,sizeof(bind_addr));
046    bind_addr.sin_family = AF_INET;
047    bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置接受任意地址
048    bind_addr.sin_port = htons(port);//将host byte order转换为network byte order
049 
050    //bind (bind socket to the created socket address)
051    if( bind(sock, (structsockaddr *)&bind_addr, sizeof(bind_addr)) == -1 ) {
052        perror("bind failed, ");
053        return1;
054    }
055    printf("bind done\n");
056 
057    //listen
058    if( listen(sock, 5) == -1) {
059        perror("listen failed.");
060        return1;
061    }
062    printf("listen done\n");
063 
064    //handler to clear zombie process
065    signal(SIGCHLD, sigCatcher);
066 
067    //loop and respond to client
068    intnew_sock;
069    intpid;
070    while(1) {
071        //wait for a connection, then accept it
072        if( (new_sock = accept(sock, NULL, NULL)) == -1 ) {
073            perror("accept failed.");
074            return1;
075        }
076        printf("accept done\n");
077 
078        pid = fork();
079        if(pid < 0) {
080            perror("fork failed");
081            return1;
082        }else if (pid == 0) {
083            //这里是子进程
084            close(sock);           //子进程中不需要server的sock
085            clientProcess(new_sock);   //使用新的new_sock和client进行交互
086            close(new_sock);       //关闭client的连接
087            exit(EXIT_SUCCESS);    //子进程退出
088        }else {
089            //这里是父进程
090            close(new_sock);//由于new_sock已经交给子进程处理,这里可以关闭了
091        }
092    }
093    return0;
094}
095 
096int clientProcess(int new_sock) {
097    intrecv_size;
098    charbuffer[BUFFER_SIZE];
099 
100    memset(buffer, 0, BUFFER_SIZE);
101    if( (recv_size = recv(new_sock, buffer, sizeof(buffer), 0)) == -1) {
102        perror("recv failed");
103        return1;
104    }
105    printf("%s\n", buffer);
106 
107    char*response = "This is the response";
108    if( send(new_sock, response, strlen(response) + 1, 0) == -1 ) {
109        perror("send failed");
110        return1;
111    }
112    return0;
113}

其中:

1signal(SIGCHLD, sigCatcher)

代码为了处理zombie process(僵尸进程)问题:当server进程运行时间较长,且产生越来越多的子进程,当这些子进程运行结束都会成为zombie process,占据系统的process table。解决方法是在父进程(server进程)中显式地处理子进程结束之后发出的SIGCHLD信号:调用wait/waitpid清理子进程的zombie信息。

测试:运行server程序,然后同时运行2个client(telnet localhost 1984),可看到该server能够很好地处理2个client。

  • 多进程方法的优点:

每个独立进程处理一个独立的client,对server进程来说只需要accept新的连接,对每个子进程来说只需要处理自己的client即可。

  • 多进程方法的缺点:

子进程的创建需要独立的父进程资源副本,开销较大,对高并发的请求不太适合;且一个进程仅处理一个client不能有效发挥作用。另外有些情况下还需要进程间进行通信以协调各进程要完成的任务。

二、non-blocking socket(单进程并发)方法

blocking socket VS non-blocking socket

默认情况下socket是blocking的,即函数accept(), recv/recvfrom, send/sendto,connect等,需等待函数执行结束之后才能够返回(此时操作系统切换到其他进程执行)。accpet()等待到有client连接请求并接受成功之后,recv/recvfrom需要读取完client发送的数据之后才能够返回。

可设置socket为non-blocking模式,即调用函数立即返回,而不是必须等待满足一定条件才返回。参看http://www.scottklement.com/rpg/socktut/nonblocking.html

non-blocking: by default, sockets are blocking – this means that they stop the function from returning until all data has been transfered. With multiple connections which may or may not be transmitting data to a server, this would not be very good as connections may have to wait to transmit their data.

设置socket为非阻塞non-blocking

使用socket()创建的socket(file descriptor),默认是阻塞的(blocking);使用函数fcntl()(file control)可设置创建的socket为非阻塞的non-blocking。

1#include <unistd.h>
2#include <fcntl.h>
3 
4sock = socket(PF_INET, SOCK_STREAM, 0);
5 
6int flags = fcntl(sock, F_GETFL, 0);
7fcntl(sock, F_SETFL, flags | O_NONBLOCK);

这样使用原本blocking的各种函数,可以立即获得返回结果。通过判断返回的errno了解状态:

  • accept():

在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示no connections没有新连接请求;

  • recv()/recvfrom():

在non-blocking模式下,如果返回值为-1,且errno == EAGAIN表示没有可接受的数据或正在接受尚未完成;

  • send()/sendto():

在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示没有可发送数据或数据发送正在进行没有完成。

  • read/write:

在non-blocking模式下,如果返回-1,且errno == EAGAIN表示没有可读写数据或可读写正在进行尚未完成。

  • connect():

在non-bloking模式下,如果返回-1,且errno = EINPROGRESS表示正在连接。

使用如上方法,可以创建一个non-blocking的server的程序,类似如下代码:

01int main(int argc, char *argv[]) {
02    intsock;
03    if( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
04        perror("socket failed");
05        return1;
06    }
07 
08    //set socket to be non-blocking
09    intflags = fcntl(sock, F_GETFL, 0);
10    fcntl(sock, F_SETFL, flags | O_NONBLOCK);
11 
12    //create socket address to bind
13    structsockaddr_in bind_addr
14    ...
15 
16    //bind
17    bind(...)
18    ...
19 
20    //listen
21    listen(...)
22    ...
23 
24    //loop
25    intnew_sock;
26    while(1) {
27        new_sock = accept(sock, NULL, NULL);
28        if(new_sock == -1 && errno== EAGAIN) {
29            fprintf(stderr,"no client connections yet\n");
30            continue;
31        }else if (new_sock == -1) {
32            perror("accept failed");
33            return1;
34        }
35 
36        //read and write
37        ...
38 
39    }  
40 
41    ...
42}

纯non-blocking程序缺点:

如果运行如上程序会发现调用accept可以理解返回,但这样会耗费大量的CPU time,实际中并不会这样使用。实际中将non-blocking和select结合使用。

三、non-blocking和select结合使用的方法

select通过轮询,监视指定file descriptor(包括socket)的变化,知道:哪些ready for reading, 哪些ready for writing,哪些发生了错误等。select和non-blocking结合使用可很好地实现socket的多client同步通信。

select函数:

1#include <sys/time.h>
2#include <sys/types.h>
3#include <unistd.h>
4 
5int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* errorfds, structtimeval* timeout);

参数说明:

  • maxfd:所有set中最大的file descriptor + 1
  • readfds:指定要侦听ready to read的file descriptor,可以为NULL
  • writefds:指定要侦听ready to write的file descriptor,可以为NULL
  • errorfds:指定要侦听errors的file descriptor,可以为NULL
  • timeout:指定侦听到期的时间长度,如果该struct timeval的各个域都为0,则相当于完全的non-blocking模式;如果该参数为NULL,相当于block模式;

select返回:

  • select返回total number of bits set in readfds, writefds and errorfds,当timeout的时候返回0,发生错误返回-1。

注:select会更新readfds(保存ready to read的file descriptor), writefds(保存read to write的fd), errorfds(保存error的fd),且更新timeout为距离超时时刻的剩余时间。

另外,fd_set类型需要使用如下4个宏进行赋值:

1FD_ZERO(fd_set *set);       //Clear all entries from the set.
2FD_SET(intfd, fd_set *set);    //Add fd to the set.
3FD_CLR(intfd, fd_set *set);    //Remove fd from the set.
4FD_ISSET(intfd, fd_set *set);  //Return true if fd is in the set.

因此通过如下代码可以将要侦听的file descriptor/socket添加到响应的fd_set中,例如:

1fd_set readfds;
2FD_ZERO(&readfds);
3 
4int sock;
5sock = socket(PF_INET, SOCK_STREAM, 0);
6 
7FD_SET(sock, &readfds);     //将新创建的socket添加到readfds中
8FD_SET(stdin, &readfds);    //将stdin添加到readfds中

struct timeval类型:

1struct timeval {
2    inttv_sec;     //seconds
3    inttv_usec;    //microseconds,注意这里是微秒不是毫秒,1秒 = 1000, 000微秒
4};

因此,使用select函数可以添加希望侦听的file descriptor/socket到read, write或error中(如果对某一项不感兴趣,可以设置为NULL),并设置每次侦听的timeout时间。

注意如果设置timeout为:

1struct timeval timeout;
2timeout.tv_sec = 0;
3timeout.tv_usec = 0;

相当于每次select立即返回相当于纯non-blocking模式;

如果设置timeout参数为NULL,则每次select持续等待到有变化则相当于blocking模式。

使用select和non-blocking实现server处理多client实例:

001#include <stdio.h>
002#include <stdlib.h>
003#include <string.h>
004#include <arpa/inet.h>
005#include <sys/types.h>
006#include <sys/socket.h>
007#include <unistd.h>
008#include <fcntl.h>
009#include <errno.h>
010#include <sys/time.h>
011 
012#define DEFAULT_PORT    1984    //默认端口
013#define BUFF_SIZE       1024    //buffer大小
014#define SELECT_TIMEOUT  5       //select的timeout seconds
015 
016//函数:设置sock为non-blocking mode
017void setSockNonBlock(int sock) {
018    intflags;
019    flags = fcntl(sock, F_GETFL, 0);
020    if(flags < 0) {
021        perror("fcntl(F_GETFL) failed");
022        exit(EXIT_FAILURE);
023    }
024    if(fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
025        perror("fcntl(F_SETFL) failed");
026        exit(EXIT_FAILURE);
027    }
028}
029//函数:更新maxfd
030int updateMaxfd(fd_set fds, int maxfd) {
031    inti;
032    intnew_maxfd = 0;
033    for(i = 0; i <= maxfd; i++) {
034        if(FD_ISSET(i, &fds) && i > new_maxfd) {
035            new_maxfd = i;
036        }
037    }
038    returnnew_maxfd;
039}
040 
041int main(int argc, char *argv[]) {
042    unsignedshort int port;
043 
044    //获取自定义端口
045    if(argc == 2) {
046        port =atoi(argv[1]);
047    } else if(argc < 2) {
048        port = DEFAULT_PORT;
049    } else {
050        fprintf(stderr,"USAGE: %s [port]\n", argv[0]);
051        exit(EXIT_FAILURE);
052    }
053 
054    //创建socket
055    intsock;
056    if( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
057        perror("socket failed, ");
058        exit(EXIT_FAILURE);
059    }
060    printf("socket done\n");
061 
062    //in case of 'address already in use' error message
063    intyes = 1;
064    if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int))) {
065        perror("setsockopt failed");
066        exit(EXIT_FAILURE);
067    }
068 
069    //设置sock为non-blocking
070    setSockNonBlock(sock);
071 
072    //创建要bind的socket address
073    structsockaddr_in bind_addr;
074    memset(&bind_addr, 0,sizeof(bind_addr));
075    bind_addr.sin_family = AF_INET;
076    bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置接受任意地址
077    bind_addr.sin_port = htons(port);//将host byte order转换为network byte order
078 
079    //bind sock到创建的socket address上
080    if( bind(sock, (structsockaddr *)&bind_addr, sizeof(bind_addr)) == -1 ) {
081        perror("bind failed, ");
082        exit(EXIT_FAILURE);
083    }
084    printf("bind done\n");
085 
086    //listen
087    if( listen(sock, 5) == -1) {
088        perror("listen failed.");
089        exit(EXIT_FAILURE);
090    }
091    printf("listen done\n");
092 
093    //创建并初始化select需要的参数(这里仅监视read),并把sock添加到fd_set中
094    fd_set readfds;
095    fd_set readfds_bak;//backup for readfds(由于每次select之后会更新readfds,因此需要backup)
096    structtimeval timeout;
097    intmaxfd;
098    maxfd = sock;
099    FD_ZERO(&readfds);
100    FD_ZERO(&readfds_bak);
101    FD_SET(sock, &readfds_bak);
102 
103    //循环接受client请求
104    intnew_sock;
105    structsockaddr_in client_addr;
106    socklen_t client_addr_len;
107    charclient_ip_str[INET_ADDRSTRLEN];
108    intres;
109    inti;
110    charbuffer[BUFF_SIZE];
111    intrecv_size;
112 
113    while(1) {
114 
115        //注意select之后readfds和timeout的值都会被修改,因此每次都进行重置
116        readfds = readfds_bak;
117        maxfd = updateMaxfd(readfds, maxfd);       //更新maxfd
118        timeout.tv_sec = SELECT_TIMEOUT;
119        timeout.tv_usec = 0;
120        printf("selecting maxfd=%d\n", maxfd);
121 
122        //select(这里没有设置writefds和errorfds,如有需要可以设置)
123        res = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
124        if(res == -1) {
125            perror("select failed");
126            exit(EXIT_FAILURE);
127        }else if (res == 0) {
128            fprintf(stderr,"no socket ready for read within %d secs\n", SELECT_TIMEOUT);
129            continue;
130        }
131 
132        //检查每个socket,并进行读(如果是sock则accept)
133        for(i = 0; i <= maxfd; i++) {
134            if(!FD_ISSET(i, &readfds)) {
135                continue;
136            }
137            //可读的socket
138            if( i == sock) {
139                //当前是server的socket,不进行读写而是accept新连接
140                client_addr_len =sizeof(client_addr);
141                new_sock = accept(sock, (structsockaddr *) &client_addr, &client_addr_len);
142                if(new_sock == -1) {
143                    perror("accept failed");
144                    exit(EXIT_FAILURE);
145                }
146                if(!inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip_str,sizeof(client_ip_str))) {
147                    perror("inet_ntop failed");
148                    exit(EXIT_FAILURE);
149                }
150                printf("accept a client from: %s\n", client_ip_str);
151                //设置new_sock为non-blocking
152                setSockNonBlock(new_sock);
153                //把new_sock添加到select的侦听中
154                if(new_sock > maxfd) {
155                    maxfd = new_sock;
156                }
157                FD_SET(new_sock, &readfds_bak);
158            }else {
159                //当前是client连接的socket,可以写(read from client)
160                memset(buffer, 0,sizeof(buffer));
161                if( (recv_size = recv(i, buffer, sizeof(buffer), 0)) == -1 ) {
162                    perror("recv failed");
163                    exit(EXIT_FAILURE);
164                }
165                printf("recved from new_sock=%d : %s(%d length string)\n", i, buffer, recv_size);
166                //立即将收到的内容写回去,并关闭连接
167                if( send(i, buffer, recv_size, 0) == -1 ) {
168                    perror("send failed");
169                    exit(EXIT_FAILURE);
170                }
171                printf("send to new_sock=%d done\n", i);
172                if( close(i) == -1 ) {
173                    perror("close failed");
174                    exit(EXIT_FAILURE);
175                }
176                printf("close new_sock=%d done\n", i);
177                //将当前的socket从select的侦听中移除
178                FD_CLR(i, &readfds_bak);
179            }
180        }
181    }
182 
183    return0;
184}

实例源码下载地址:http://velep.com/downloads?did=16,经测试可用!

编译并运行如上程序,然后尝试使用多个telnet localhost 1984连接该server。可以发现各个connection很好地独立工作。因此,使用select可实现一个进程尽最大所能地处理尽可能多的client。

参考资料:

以上文章内容参考:http://blog.csdn.net/haibinglong/article/details/6862360    

本文转自:

http://velep.com/archives/1137.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Windows下,可以使用select函数来检测客户端连接是否断开。select函数可以监视多个套接字的状态,当有套接字状态发生变化时,select函数就会返回。如果客户端连接断开,相应的套接字状态也会发生变化,通过检测套接字状态可以判断客户端连接是否断开。 以下是使用select函数检测客户端连接是否断开的示例代码: ```c #include <winsock2.h> #include <stdio.h> int main() { // 初始化Winsock WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed.\n"); return 1; } // 创建监听套接字 SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock == INVALID_SOCKET) { printf("socket failed.\n"); WSACleanup(); return 1; } // 绑定监听套接字 sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(12345); addr.sin_addr.s_addr = INADDR_ANY; if (bind(listenSock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { printf("bind failed.\n"); closesocket(listenSock); WSACleanup(); return 1; } // 开始监听 if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) { printf("listen failed.\n"); closesocket(listenSock); WSACleanup(); return 1; } // 创建fd_set并将监听套接字添加进去 fd_set readSet; FD_ZERO(&readSet); FD_SET(listenSock, &readSet); // 循环等待客户端连接 while (true) { // 调用select函数等待客户端连接或数据到达 fd_set tempSet = readSet; int ret = select(0, &tempSet, NULL, NULL, NULL); if (ret == SOCKET_ERROR) { printf("select failed.\n"); break; } // 检测监听套接字是否有连接请求 if (FD_ISSET(listenSock, &tempSet)) { SOCKET clientSock = accept(listenSock, NULL, NULL); if (clientSock == INVALID_SOCKET) { printf("accept failed.\n"); break; } printf("client connected.\n"); // 将新连接的客户端套接字添加进fd_set FD_SET(clientSock, &readSet); } // 检测客户端套接字是否有数据到达或连接断开 for (int i = 0; i < readSet.fd_count; i++) { SOCKET sock = readSet.fd_array[i]; if (sock != listenSock && FD_ISSET(sock, &tempSet)) { char buf[1024]; int ret = recv(sock, buf, sizeof(buf), 0); if (ret == SOCKET_ERROR || ret == 0) { // 客户端连接断开,将套接字从fd_set中删除 printf("client disconnected.\n"); closesocket(sock); FD_CLR(sock, &readSet); } else { // 处理接收到的数据 // ... } } } } // 关闭监听套接字 closesocket(listenSock); // 清理Winsock WSACleanup(); return 0; } ``` 注意:以上代码仅为示例代码,实际使用时还需要进行错误处理和异常情况处理等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值