天天看点

服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现

问题聚焦: 当客户端阻塞于从标准输入接收数据时,将读取不到别的途径发过来的必要信息,如TCP发过来的FIN标志。 因此,进程需要内核一旦发现进程指定的一个或多个IO条件就绪(即输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。 这个机制称为I/O复用,这是由select, poll, epoll函数支持的。

编译环境:     Ubuntu12.04  g++

需求描述:

  1. 单进程,IO复用,实现多个连接同时监听和收发信息
  2. 当服务器进程一终止,客户就能马上得到结果(select +shutdown实现)
  3. 当客户端使用"exit"命令或者Cirl+C结束进程时,服务器可以立即感应到,并关闭当前接口(select+close实现

来看一下实现后的运行效果:

服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现

步骤:

  1. 服务器连接了第一个客户,并收发消息“hello world”
  2. 服务器连接了第二个客户,并收发消息“hello select”
  3. 服务器从第一个客户收发消息“hello world again”
  4. 服务器从第二个客户收发消息“hello select again”
  5. 第二个客户关闭连接
  6. 第一个客户关闭连接

在了解select实现之前,先复习一下之前了解的IO模型

五种IO模型

  • 阻塞式IO
  • 非阻塞式IO
  • IO复用
  • 信号驱动式IO
  • 异步IO

对比:

服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于TCP来说,这两步分别为:

  • 等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区
  • 把数据从内核缓冲区复制到应用进程缓冲区

select api相关请参考: http://blog.csdn.net/zs634134578/article/details/19929449

流程:

  1. select主要通过维护两个数组,来实现端口的轮询:
  2. client[]数组,记录有哪些连接已经建立
  3. rset[]数组,记录有注册哪些端口,需要监听
  4. 当rset数组中注册的端口被激活,这时将端口号放到client数组中,稍后遍历client[]数组,处理连接上的数据

代码实现: 源码:https://github.com/zs634134578/UNP

服务器端:

#include "mtserver.h"

int main(int argc, char* argv[])
{
    checkArgc(argc, 2);
    
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    /* declare socket*/
    int listenfd, connfd, sockfd;
    int ret;
    
    /* initialize listen socket*/
    mySocket(listenfd);
    
    /* server address */
    struct sockaddr_in servaddr;
    initSockAddr(servaddr, ip, port);
    
    /* bind */
    myBind(listenfd,
	 	   (struct sockaddr*)&servaddr,
           sizeof(servaddr));
    
    /* listen */
    myListen(listenfd, 5);
    
    /* handle SIGCHLD signal*/
    //signal(SIGCHLD, handle_sigchild);
    
    /* waiting for connecting */
    pid_t chipid;
    socklen_t clilen;
    struct sockaddr_in cliaddr;

	/* select initialize */
	int maxfd, maxi, i;
	bool toclose;
	int nready, client[FD_SETSIZE];
	fd_set rset, allset;
	
	maxfd = listenfd;
	maxi = -1;
	for ( i=0; i < FD_SETSIZE; i++ )
		client[i] = -1;

	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);
    
	printf("Waiting for connecting...\n");
	
    for(;;) {
		rset = allset;
		if ( (nready=select(maxfd+1, &rset, NULL, NULL, NULL)) < 0 ) {
			fprintf(stderr,
					"select failed.%s\n",
					strerror(errno));
			continue;
		}
		
		/* handle listen fd and no recv or respond */
		if (FD_ISSET(listenfd, &rset)) {
			clilen = sizeof(cliaddr);
			connfd = myAccept(listenfd,
							  (struct sockaddr*)&cliaddr,
							  &clilen);
			printf("Connection is established with sockfd: %d\n",
				   connfd);
			for ( i = 0; i < FD_SETSIZE; i++) {
				if ( client[i] < 0 ) {
					client[i] = connfd;
					break;
				}
			}
			
			if (i == FD_SETSIZE) {
				fprintf(stderr,
						"too many clients\n"
						);
				break;
			}
			
			FD_SET( connfd, &allset );
			if ( connfd > maxfd ) {
				maxfd = connfd;
			}
			if ( i > maxi) {
				maxi = i;
			}
			
			if (--nready <= 0) {
				continue;
			}
		}
		
		/* handle accept fds(client[]) and handle recv or respond msg */
		for ( i = 0; i <= maxi; i++) {
			if ( (sockfd = client[i]) < 0 )
				continue;
			if ( FD_ISSET(sockfd, &rset) ) {
				if( (toclose = handle_recv(sockfd))) {
					printf("Client close this connection: %d\n" ,
						   sockfd);
					close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				}
				
				if (--nready <= 0) 
					break;
			}
		}
    }
}


bool handle_recv(int connfd) {
     
    char recvbuf[BUFSIZE];

	memset( recvbuf, '\0', BUFSIZE );
	if ( recv(connfd, recvbuf,BUFSIZE,0) != 0) {
		if (!strcmp(recvbuf, "exit"))
			return true;
		fprintf(stderr,"recv msg: \"%s\" from connfd:%d\n", recvbuf, connfd);
		send(connfd, recvbuf, strlen(recvbuf), 0);
		fprintf(stderr,"send back: \"%s\" to connfd:%d\n\n", recvbuf, connfd);
	}
	else
		return true;
	return false;
}
           

客户端:

#include "mtclient.h"

int main(int argc, char* argv[])
{   
	checkArgc(argc, 2);
    
    int port = atoi(argv[2]);
    char* ip = argv[1];
    

    int sockfd;
    struct sockaddr_in servaddr;

    mySocket(sockfd);

    initSockAddr(servaddr,ip, port);
    
    myConnect(sockfd,
              (struct sockaddr*)&servaddr,
              sizeof(servaddr));
    
    handle_msg(sockfd);
    exit(0);
    
}


void handle_msg(int sockfd) {

    char sendbuf[BUFSIZE];
    char recvbuf[BUFSIZE];
    
    int maxfdpl, ret;
    fd_set rset;
	int normalTermi = 0;
    
    FD_ZERO(&rset);

    while(1) {
	 	memset( sendbuf, '\0', BUFSIZE );
        memset( recvbuf, '\0', BUFSIZE );
        
		if (normalTermi == 0)
			FD_SET( 0, &rset );

        FD_SET( sockfd, &rset );		
		maxfdpl = sockfd + 1;

		if(DEBUG)
			printf("Debug: waiting in select\n");
		if ( select( maxfdpl, &rset, NULL, NULL, NULL) < 0 ) {
			fprintf(stderr,
					"select failed.%s\n",
					strerror(errno));
		}
		if(DEBUG)
			printf("Debug: after select\n");

		if (FD_ISSET( sockfd, &rset )) {
			if (recv(sockfd, recvbuf, BUFSIZE, 0) == 0) {

				if(DEBUG)
					printf("Debug: ready to quit, normalTermi: %d\n" ,
						   normalTermi);

				if (normalTermi == 1) {
					printf("handle_msg: normal terminated.\n");
					return;
				}
				else {
					printf("handle_msg: server terminated.\n");
					exit(0);
				}
			}
			fprintf(stderr,
					"recv back: %s\n",
					recvbuf);
		}
		else if ( FD_ISSET( 0, &rset ) ) {
			gets(sendbuf);
			if (strlen(sendbuf) > 0) {
				send(sockfd, sendbuf, strlen(sendbuf), 0);
				if ( !strcmp(sendbuf, "exit") ) {
					normalTermi = 1;
					shutdown(sockfd, SHUT_WR);
					FD_CLR(0, &rset);
					continue;
				}
			}
		}
    }
    close( sockfd );
    return;
}
           

问题: 1 监听标准输入的描述符? 解决:标准输入描述符:0

2 当客户端发送所有消息,即可关闭连接,但是如果这时候调用close方法,会导致接收不到仍在传送过来的信息。 方案:需要一种关闭TCP连接其中一半的方法,也即是说,我们想给服务器发送一个FIN,告诉它我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。 完成这个功能的函数为shutdown。 shutdown函数可以不管描述符的引用计数,就激发TCP的正常连接终止序列。 关闭一半的图示:

函数声明: #include <sys/socket.h> int shutdown(int sockfd, int howto); howto:取值SHUT_RD(关闭这一端的读,不再读取连接上的数据)              SHUT_WR(关闭这一端的写,不再往连接上写数据)              SHUT_RDWR(关闭这一端的读和写)

3 套接字描述符的第一个可用描述符是多少? 答案:3。0 1 2分别为标准输入,标准输出,标准错误输出。

4 服务器进程终止后的动作? 这里需要知道的一点是,当服务器进程一终止,就会对客户进程发送一个FIN信号,这时套接字连接可读,read返回0

参考资料: 《Linux高性能服务器编程》 《UNIX网络编程 卷1:套接字联网API(第3版)》

继续阅读