IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    reille发表于 2015-05-31 02:50:18
    love 0

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

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

    一、多进程/线程方法

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

    #include 
    #include 
    #include 
    #include inet.h>
    #include <systypes.h>
    #include socket.h>
    #include <unistd.h>
    
    #include <signal.h>
    #include <syswait.h>
    
    #define DEFAULT_PORT    1984    //默认端口
    #define BUFFER_SIZE     1024    //buffer大小
    
    void sigCatcher(int n) {
    	//printf("a child process dies\n");
    	while(waitpid(-1, NULL, WNOHANG) > 0);
    }
    
    int clientProcess(int new_sock);
    
    int main(int argc, char *argv[]) {
    	unsigned short int port;
    
    	//get port, use default if not set
    	if (argc == 2) {
    		port = atoi(argv[1]);
    	} else if (argc < 2) {
    		port = DEFAULT_PORT;
    	} else {
    		fprintf(stderr, "USAGE: %s [port]\n", argv[0]);
    		return 1;
    	}
    
    	//create socket
    	int sock;
    	if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
    		perror("socket failed, ");
    		return 1;
    	}
    	printf("socket done\n");
    
    	//create socket address and initialize
    	struct sockaddr_in bind_addr;
    	memset(&bind;_addr, 0, sizeof(bind_addr));
    	bind_addr.sin_family = AF_INET;
    	bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //设置接受任意地址
    	bind_addr.sin_port = htons(port);//将host byte order转换为network byte order
    
    	//bind (bind socket to the created socket address)
    	if ( bind(sock, (struct sockaddr *)&bind;_addr, sizeof(bind_addr)) == -1 ) {
    		perror("bind failed, ");
    		return 1;
    	}
    	printf("bind done\n");
    
    	//listen
    	if ( listen(sock, 5) == -1) {
    		perror("listen failed.");
    		return 1;
    	}
    	printf("listen done\n");
    
    	//handler to clear zombie process
    	signal(SIGCHLD, sigCatcher);
    
    	//loop and respond to client
    	int new_sock;
    	int pid;
    	while (1) {
    		//wait for a connection, then accept it
    		if ( (new_sock = accept(sock, NULL, NULL)) == -1 ) {
    			perror("accept failed.");
    			return 1;
    		}
    		printf("accept done\n");
    
    		pid = fork();
    		if (pid < 0) {
    			perror("fork failed");
    			return 1;
    		} else if (pid == 0) {
    			//这里是子进程
    			close(sock);            //子进程中不需要server的sock
    			clientProcess(new_sock);    //使用新的new_sock和client进行交互
    			close(new_sock);        //关闭client的连接
    			exit(EXIT_SUCCESS);     //子进程退出
    		} else {
    			//这里是父进程
    			close(new_sock); //由于new_sock已经交给子进程处理,这里可以关闭了
    		}
    	}
    	return 0;
    }
    
    int clientProcess(int new_sock) {
    	int recv_size;
    	char buffer[BUFFER_SIZE];
    
    	memset(buffer, 0, BUFFER_SIZE);
    	if ( (recv_size = recv(new_sock, buffer, sizeof(buffer), 0)) == -1) {
    		perror("recv failed");
    		return 1;
    	}
    	printf("%s\n", buffer);
    
    	char *response = "This is the response";
    	if ( send(new_sock, response, strlen(response) + 1, 0) == -1 ) {
    		perror("send failed");
    		return 1;
    	}
    	return 0;
    }

    其中:

    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。

    #include 
    #include 
    
    sock = socket(PF_INET, SOCK_STREAM, 0);
    
    int flags = fcntl(sock, F_GETFL, 0);
    fcntl(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的程序,类似如下代码:

    int main(int argc, char *argv[]) {
    	int sock;
    	if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
    		perror("socket failed");
    		return 1;
    	}
    
    	//set socket to be non-blocking
    	int flags = fcntl(sock, F_GETFL, 0);
    	fcntl(sock, F_SETFL, flags | O_NONBLOCK);
    
    	//create socket address to bind
    	struct sockaddr_in bind_addr
    	...
    
    	//bind
    	bind(...)
    	...
    
    	//listen
    	listen(...)
    	...
    
    	//loop 
    	int new_sock;
    	while (1) {
    		new_sock = accept(sock, NULL, NULL);
    		if (new_sock == -1 && errno == EAGAIN) {
    			fprintf(stderr, "no client connections yet\n");
    			continue;
    		} else if (new_sock == -1) {
    			perror("accept failed");
    			return 1;
    		}
    
    		//read and write
    		...
    
    	}   
    
    	...
    }

    纯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函数:

    #include time.h>
    #include <systypes.h>
    #include 
    
    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个宏进行赋值:

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

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

    fd_set readfds;
    FD_ZERO(&readfds;);
    
    int sock;
    sock = socket(PF_INET, SOCK_STREAM, 0);
    
    FD_SET(sock, &readfds;);     //将新创建的socket添加到readfds中
    FD_SET(stdin, &readfds;);    //将stdin添加到readfds中

    struct timeval类型:

    struct timeval {
    	int tv_sec;     //seconds
    	int tv_usec;    //microseconds,注意这里是微秒不是毫秒,1秒 = 1000, 000微秒
    };

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

    注意如果设置timeout为:

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

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

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

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

    #include 
    #include 
    #include 
    #include inet.h>
    #include <systypes.h>
    #include socket.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <systime.h>
    
    #define DEFAULT_PORT    1984    //默认端口
    #define BUFF_SIZE       1024    //buffer大小
    #define SELECT_TIMEOUT  5       //select的timeout seconds
    
    //函数:设置sock为non-blocking mode
    void setSockNonBlock(int sock) {
    	int flags;
    	flags = fcntl(sock, F_GETFL, 0);
    	if (flags < 0) {
    		perror("fcntl(F_GETFL) failed");
    		exit(EXIT_FAILURE);
    	}
    	if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
    		perror("fcntl(F_SETFL) failed");
    		exit(EXIT_FAILURE);
    	}
    }
    //函数:更新maxfd
    int updateMaxfd(fd_set fds, int maxfd) {
    	int i;
    	int new_maxfd = 0;
    	for (i = 0; i <= maxfd; i++) {
    		if (FD_ISSET(i, &fds;) && i > new_maxfd) {
    			new_maxfd = i;
    		}
    	}
    	return new_maxfd;
    }
    
    int main(int argc, char *argv[]) {
    	unsigned short int port;
    
    	//获取自定义端口
    	if (argc == 2) {
    		port = atoi(argv[1]);
    	} else if (argc < 2) {
    		port = DEFAULT_PORT;
    	} else {
    		fprintf(stderr, "USAGE: %s [port]\n", argv[0]);
    		exit(EXIT_FAILURE);
    	}
    
    	//创建socket
    	int sock;
    	if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
    		perror("socket failed, ");
    		exit(EXIT_FAILURE);
    	}
    	printf("socket done\n");
    
    	//in case of 'address already in use' error message
    	int yes = 1;
    	if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes;, sizeof(int))) {
    		perror("setsockopt failed");
    		exit(EXIT_FAILURE);
    	}
    
    	//设置sock为non-blocking
    	setSockNonBlock(sock);
    
    	//创建要bind的socket address
    	struct sockaddr_in bind_addr;
    	memset(&bind;_addr, 0, sizeof(bind_addr));
    	bind_addr.sin_family = AF_INET;
    	bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //设置接受任意地址
    	bind_addr.sin_port = htons(port);//将host byte order转换为network byte order
    
    	//bind sock到创建的socket address上
    	if ( bind(sock, (struct sockaddr *)&bind;_addr, sizeof(bind_addr)) == -1 ) {
    		perror("bind failed, ");
    		exit(EXIT_FAILURE);
    	}
    	printf("bind done\n");
    
    	//listen
    	if ( listen(sock, 5) == -1) {
    		perror("listen failed.");
    		exit(EXIT_FAILURE);
    	}
    	printf("listen done\n");
    
    	//创建并初始化select需要的参数(这里仅监视read),并把sock添加到fd_set中
    	fd_set readfds;
    	fd_set readfds_bak; //backup for readfds(由于每次select之后会更新readfds,因此需要backup)
    	struct timeval timeout;
    	int maxfd;
    	maxfd = sock;
    	FD_ZERO(&readfds;);
    	FD_ZERO(&readfds;_bak);
    	FD_SET(sock, &readfds;_bak);
    
    	//循环接受client请求
    	int new_sock;
    	struct sockaddr_in client_addr;
    	socklen_t client_addr_len;
    	char client_ip_str[INET_ADDRSTRLEN];
    	int res;
    	int i;
    	char buffer[BUFF_SIZE];
    	int recv_size;
    
    	while (1) {
    
    		//注意select之后readfds和timeout的值都会被修改,因此每次都进行重置
    		readfds = readfds_bak;
    		maxfd = updateMaxfd(readfds, maxfd);        //更新maxfd
    		timeout.tv_sec = SELECT_TIMEOUT;
    		timeout.tv_usec = 0;
    		printf("selecting maxfd=%d\n", maxfd);
    
    		//select(这里没有设置writefds和errorfds,如有需要可以设置)
    		res = select(maxfd + 1, &readfds;, NULL, NULL, &timeout;);
    		if (res == -1) {
    			perror("select failed");
    			exit(EXIT_FAILURE);
    		} else if (res == 0) {
    			fprintf(stderr, "no socket ready for read within %d secs\n", SELECT_TIMEOUT);
    			continue;
    		}
    
    		//检查每个socket,并进行读(如果是sock则accept)
    		for (i = 0; i <= maxfd; i++) {
    			if (!FD_ISSET(i, &readfds;)) {
    				continue;
    			}
    			//可读的socket
    			if ( i == sock) {
    				//当前是server的socket,不进行读写而是accept新连接
    				client_addr_len = sizeof(client_addr);
    				new_sock = accept(sock, (struct sockaddr *) &client;_addr, &client;_addr_len);
    				if (new_sock == -1) {
    					perror("accept failed");
    					exit(EXIT_FAILURE);
    				}
    				if (!inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip_str, sizeof(client_ip_str))) {
    					perror("inet_ntop failed");
    					exit(EXIT_FAILURE);
    				}
    				printf("accept a client from: %s\n", client_ip_str);
    				//设置new_sock为non-blocking
    				setSockNonBlock(new_sock);
    				//把new_sock添加到select的侦听中
    				if (new_sock > maxfd) {
    					maxfd = new_sock;
    				}
    				FD_SET(new_sock, &readfds;_bak);
    			} else {
    				//当前是client连接的socket,可以写(read from client)
    				memset(buffer, 0, sizeof(buffer));
    				if ( (recv_size = recv(i, buffer, sizeof(buffer), 0)) == -1 ) {
    					perror("recv failed");
    					exit(EXIT_FAILURE);
    				}
    				printf("recved from new_sock=%d : %s(%d length string)\n", i, buffer, recv_size);
    				//立即将收到的内容写回去,并关闭连接
    				if ( send(i, buffer, recv_size, 0) == -1 ) {
    					perror("send failed");
    					exit(EXIT_FAILURE);
    				}
    				printf("send to new_sock=%d done\n", i);
    				if ( close(i) == -1 ) {
    					perror("close failed");
    					exit(EXIT_FAILURE);
    				}
    				printf("close new_sock=%d done\n", i);
    				//将当前的socket从select的侦听中移除
    				FD_CLR(i, &readfds;_bak);
    			}
    		}
    	}
    
    	return 0;
    }

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

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

    参考资料:

    • http://www.linuxhowtos.org/C_C++/socket.htm
    • http://beej.us/guide/bgnet/output/html/multipage/clientserver.html
    • http://beej.us/guide/bgnet/output/html/multipage/advanced.html
    • http://www.scottklement.com/rpg/socktut/nonblocking.html
    • http://en.wikipedia.org/wiki/Select_%28Unix%29
    • http://www.lowtek.com/sockets/select.html
    • http://systhread.net/texts/200909netconn4.php
    • http://www.tenouk.com/Module41.html
    • http://stackoverflow.com/questions/1150635/unix-nonblocking-i-o-o-nonblock-vs-fionbio
    • http://stackoverflow.com/questions/1735781/non-blocking-pipe-using-popen
    • http://www.kegel.com/dkftpbench/nonblocking.html

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

    您可能也喜欢:

    UDP sendto和recvfrom使用详解

    linux shell脚本编程1—零星总结

    linux shell脚本编程2—修改文件时间和创建新文件即touch命令的使用

    Linux kernel路由机制分析(下)

    linux内核container_of详解
    无觅
    » 本文地址: http://velep.com/archives/1137.html
    » 文章出处: reille博客—http://velep.com , 如果没有特别声明,文章均为reille博客原创作品
    » 郑重声明: 原创作品未经允许不得转载,如需转载请联系reille#qq.com(#换成@)
    分享到:
    推荐阅读相关文章:
    • UDP sendto和recvfrom使用详解
    • 联想E430C和WINDOW 7
    • undefined reference to `_WinMain@16′问题
    • linux backtrace()详细使用说明,分析Segmentation fault
    • 一个PTHREAD_STACK_MIN宏定义引出的头文件包含问题
    • linux性能分析工具oprofile的安装与使用
    • linux下统计代码执行时间
    • 心仪已久的树莓派终于到手了


沪ICP备19023445号-2号
友情链接