参看基于TCP/UDP的socket代码,同一时间Server只能处理一个Client请求:在使用当前连接的socket和client进行交互的时候,不能够accept新的连接请求。为了使Server能够处理多个Client请求,常见的方法:
多进程/线程方法、non-blocking socket(单进程并发)、non-blocking和select结合使用。三种方法各有优缺点,下面进行详细分析和说明。
一、多进程/线程方法
这种方法,每个子进程/线程单独处理一个client连接。以使用进程为例,在每个accept成功之后,使用fork创建一个子进程专门处理该client的connection,父进程(server)本身可以继续accept其他新的client的连接请求。示例代码如下:
004 | #include <arpa/inet.h> |
005 | #include <sys/types.h> |
006 | #include <sys/socket.h> |
012 | #define DEFAULT_PORT 1984 //默认端口 |
013 | #define BUFFER_SIZE 1024 //buffer大小 |
015 | void sigCatcher( int n) { |
017 | while (waitpid(-1, NULL, WNOHANG) > 0); |
020 | int clientProcess( int new_sock); |
022 | int main( int argc, char *argv[]) { |
023 | unsigned short int port; |
028 | } else if (argc < 2) { |
031 | fprintf (stderr, "USAGE: %s [port]\n" , argv[0]); |
037 | if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) { |
038 | perror ( "socket failed, " ); |
041 | printf ( "socket done\n" ); |
044 | struct sockaddr_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); |
051 | if ( bind(sock, ( struct sockaddr *)&bind_addr, sizeof (bind_addr)) == -1 ) { |
052 | perror ( "bind failed, " ); |
055 | printf ( "bind done\n" ); |
058 | if ( listen(sock, 5) == -1) { |
059 | perror ( "listen failed." ); |
062 | printf ( "listen done\n" ); |
065 | signal (SIGCHLD, sigCatcher); |
072 | if ( (new_sock = accept(sock, NULL, NULL)) == -1 ) { |
073 | perror ( "accept failed." ); |
076 | printf ( "accept done\n" ); |
080 | perror ( "fork failed" ); |
082 | } else if (pid == 0) { |
085 | clientProcess(new_sock); |
096 | int clientProcess( int new_sock) { |
098 | char buffer[BUFFER_SIZE]; |
100 | memset (buffer, 0, BUFFER_SIZE); |
101 | if ( (recv_size = recv(new_sock, buffer, sizeof (buffer), 0)) == -1) { |
102 | perror ( "recv failed" ); |
105 | printf ( "%s\n" , buffer); |
107 | char *response = "This is the response" ; |
108 | if ( send(new_sock, response, strlen (response) + 1, 0) == -1 ) { |
109 | perror ( "send failed" ); |
其中:
1 | signal (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。
4 | sock = socket(PF_INET, SOCK_STREAM, 0); |
6 | int flags = fcntl(sock, F_GETFL, 0); |
7 | fcntl(sock, F_SETFL, flags | O_NONBLOCK); |
这样使用原本blocking的各种函数,可以立即获得返回结果。通过判断返回的errno了解状态:
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示no connections没有新连接请求;
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN表示没有可接受的数据或正在接受尚未完成;
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示没有可发送数据或数据发送正在进行没有完成。
在non-blocking模式下,如果返回-1,且errno == EAGAIN表示没有可读写数据或可读写正在进行尚未完成。
在non-bloking模式下,如果返回-1,且errno = EINPROGRESS表示正在连接。
使用如上方法,可以创建一个non-blocking的server的程序,类似如下代码:
01 | int main( int argc, char *argv[]) { |
03 | if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) { |
04 | perror ( "socket failed" ); |
09 | int flags = fcntl(sock, F_GETFL, 0); |
10 | fcntl(sock, F_SETFL, flags | O_NONBLOCK); |
13 | struct sockaddr_in bind_addr |
27 | new_sock = accept(sock, NULL, NULL); |
28 | if (new_sock == -1 && errno == EAGAIN) { |
29 | fprintf (stderr, "no client connections yet\n" ); |
31 | } else if (new_sock == -1) { |
32 | perror ( "accept failed" ); |
纯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函数:
5 | int select( int maxfd, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* 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个宏进行赋值:
2 | FD_SET( int fd, fd_set *set); |
3 | FD_CLR( int fd, fd_set *set); |
4 | FD_ISSET( int fd, fd_set *set); |
因此通过如下代码可以将要侦听的file descriptor/socket添加到响应的fd_set中,例如:
5 | sock = socket(PF_INET, SOCK_STREAM, 0); |
8 | FD_SET(stdin, &readfds); |
struct timeval类型:
因此,使用select函数可以添加希望侦听的file descriptor/socket到read, write或error中(如果对某一项不感兴趣,可以设置为NULL),并设置每次侦听的timeout时间。
注意如果设置timeout为:
相当于每次select立即返回相当于纯non-blocking模式;
如果设置timeout参数为NULL,则每次select持续等待到有变化则相当于blocking模式。
使用select和non-blocking实现server处理多client实例:
004 | #include <arpa/inet.h> |
005 | #include <sys/types.h> |
006 | #include <sys/socket.h> |
012 | #define DEFAULT_PORT 1984 //默认端口 |
013 | #define BUFF_SIZE 1024 //buffer大小 |
014 | #define SELECT_TIMEOUT 5 //select的timeout seconds |
017 | void setSockNonBlock( int sock) { |
019 | flags = fcntl(sock, F_GETFL, 0); |
021 | perror ( "fcntl(F_GETFL) failed" ); |
024 | if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) { |
025 | perror ( "fcntl(F_SETFL) failed" ); |
030 | int updateMaxfd(fd_set fds, int maxfd) { |
033 | for (i = 0; i <= maxfd; i++) { |
034 | if (FD_ISSET(i, &fds) && i > new_maxfd) { |
041 | int main( int argc, char *argv[]) { |
042 | unsigned short int port; |
047 | } else if (argc < 2) { |
050 | fprintf (stderr, "USAGE: %s [port]\n" , argv[0]); |
056 | if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) { |
057 | perror ( "socket failed, " ); |
060 | printf ( "socket done\n" ); |
064 | if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof ( int ))) { |
065 | perror ( "setsockopt failed" ); |
070 | setSockNonBlock(sock); |
073 | struct sockaddr_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); |
080 | if ( bind(sock, ( struct sockaddr *)&bind_addr, sizeof (bind_addr)) == -1 ) { |
081 | perror ( "bind failed, " ); |
084 | printf ( "bind done\n" ); |
087 | if ( listen(sock, 5) == -1) { |
088 | perror ( "listen failed." ); |
091 | printf ( "listen done\n" ); |
096 | struct timeval timeout; |
100 | FD_ZERO(&readfds_bak); |
101 | FD_SET(sock, &readfds_bak); |
105 | struct sockaddr_in client_addr; |
106 | socklen_t client_addr_len; |
107 | char client_ip_str[INET_ADDRSTRLEN]; |
110 | char buffer[BUFF_SIZE]; |
116 | readfds = readfds_bak; |
117 | maxfd = updateMaxfd(readfds, maxfd); |
118 | timeout.tv_sec = SELECT_TIMEOUT; |
120 | printf ( "selecting maxfd=%d\n" , maxfd); |
123 | res = select(maxfd + 1, &readfds, NULL, NULL, &timeout); |
125 | perror ( "select failed" ); |
127 | } else if (res == 0) { |
128 | fprintf (stderr, "no socket ready for read within %d secs\n" , SELECT_TIMEOUT); |
133 | for (i = 0; i <= maxfd; i++) { |
134 | if (!FD_ISSET(i, &readfds)) { |
140 | client_addr_len = sizeof (client_addr); |
141 | new_sock = accept(sock, ( struct sockaddr *) &client_addr, &client_addr_len); |
143 | perror ( "accept failed" ); |
146 | if (!inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip_str, sizeof (client_ip_str))) { |
147 | perror ( "inet_ntop failed" ); |
150 | printf ( "accept a client from: %s\n" , client_ip_str); |
152 | setSockNonBlock(new_sock); |
154 | if (new_sock > maxfd) { |
157 | FD_SET(new_sock, &readfds_bak); |
160 | memset (buffer, 0, sizeof (buffer)); |
161 | if ( (recv_size = recv(i, buffer, sizeof (buffer), 0)) == -1 ) { |
162 | perror ( "recv failed" ); |
165 | printf ( "recved from new_sock=%d : %s(%d length string)\n" , i, buffer, recv_size); |
167 | if ( send(i, buffer, recv_size, 0) == -1 ) { |
168 | perror ( "send failed" ); |
171 | printf ( "send to new_sock=%d done\n" , i); |
172 | if ( close(i) == -1 ) { |
173 | perror ( "close failed" ); |
176 | printf ( "close new_sock=%d done\n" , i); |
178 | FD_CLR(i, &readfds_bak); |
实例源码下载地址: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