服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现
问题聚焦:
当客户端阻塞于从标准输入接收数据时,将读取不到别的途径发过来的必要信息,如TCP发过来的FIN标志。
因此,进程需要内核一旦发现进程指定的一个或多个IO条件就绪(即输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。
这个机制称为I/O复用,这是由select, poll, epoll函数支持的。
编译环境:
Ubuntu12.04 g++
需求描述:
- 单进程,IO复用,实现多个连接同时监听和收发信息
- 当服务器进程一终止,客户就能马上得到结果(select +shutdown实现)
- 当客户端使用"exit"命令或者Cirl+C结束进程时,服务器可以立即感应到,并关闭当前接口(select+close实现
来看一下实现后的运行效果:
步骤:
- 服务器连接了第一个客户,并收发消息“hello world”
- 服务器连接了第二个客户,并收发消息“hello select”
- 服务器从第一个客户收发消息“hello world again”
- 服务器从第二个客户收发消息“hello select again”
- 第二个客户关闭连接
- 第一个客户关闭连接
在了解select实现之前,先复习一下之前了解的IO模型
五种IO模型
- 阻塞式IO
- 非阻塞式IO
- IO复用
- 信号驱动式IO
- 异步IO
对比:
一个输入操作通常包括两个不同的阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于TCP来说,这两步分别为:
- 等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区
- 把数据从内核缓冲区复制到应用进程缓冲区
select
流程:
- select主要通过维护两个数组,来实现端口的轮询:
- client[]数组,记录有哪些连接已经建立
- rset[]数组,记录有注册哪些端口,需要监听
- 当rset数组中注册的端口被激活,这时将端口号放到client数组中,稍后遍历client[]数组,处理连接上的数据
代码实现:
服务器端:
1 #include "mtserver.h" 2 3 int main(int argc, char* argv[]) 4 { 5 checkArgc(argc, 2); 6 7 const char* ip = argv[1]; 8 int port = atoi( argv[2] ); 9 10 /* declare socket*/ 11 int listenfd, connfd, sockfd; 12 int ret; 13 14 /* initialize listen socket*/ 15 mySocket(listenfd); 16 17 /* server address */ 18 struct sockaddr_in servaddr; 19 initSockAddr(servaddr, ip, port); 20 21 /* bind */ 22 myBind(listenfd, 23 (struct sockaddr*)&servaddr, 24 sizeof(servaddr)); 25 26 /* listen */ 27 myListen(listenfd, 5); 28 29 /* handle SIGCHLD signal*/ 30 //signal(SIGCHLD, handle_sigchild); 31 32 /* waiting for connecting */ 33 pid_t chipid; 34 socklen_t clilen; 35 struct sockaddr_in cliaddr; 36 37 /* select initialize */ 38 int maxfd, maxi, i; 39 bool toclose; 40 int nready, client[FD_SETSIZE]; 41 fd_set rset, allset; 42 43 maxfd = listenfd; 44 maxi = -1; 45 for ( i=0; i < FD_SETSIZE; i++ ) 46 client[i] = -1; 47 48 FD_ZERO(&allset); 49 FD_SET(listenfd, &allset); 50 51 printf("Waiting for connecting...\n"); 52 53 for(;;) { 54 rset = allset; 55 if ( (nready=select(maxfd+1, &rset, NULL, NULL, NULL)) < 0 ) { 56 fprintf(stderr, 57 "select failed.%s\n", 58 strerror(errno)); 59 continue; 60 } 61 62 /* handle listen fd and no recv or respond */ 63 if (FD_ISSET(listenfd, &rset)) { 64 clilen = sizeof(cliaddr); 65 connfd = myAccept(listenfd, 66 (struct sockaddr*)&cliaddr, 67 &clilen); 68 printf("Connection is established with sockfd: %d\n", 69 connfd); 70 for ( i = 0; i < FD_SETSIZE; i++) { 71 if ( client[i] < 0 ) { 72 client[i] = connfd; 73 break; 74 } 75 } 76 77 if (i == FD_SETSIZE) { 78 fprintf(stderr, 79 "too many clients\n" 80 ); 81 break; 82 } 83 84 FD_SET( connfd, &allset ); 85 if ( connfd > maxfd ) { 86 maxfd = connfd; 87 } 88 if ( i > maxi) { 89 maxi = i; 90 } 91 92 if (--nready <= 0) { 93 continue; 94 } 95 } 96 97 /* handle accept fds(client[]) and handle recv or respond msg */ 98 for ( i = 0; i <= maxi; i++) { 99 if ( (sockfd = client[i]) < 0 ) 100 continue; 101 if ( FD_ISSET(sockfd, &rset) ) { 102 if( (toclose = handle_recv(sockfd))) { 103 printf("Client close this connection: %d\n" , 104 sockfd); 105 close(sockfd); 106 FD_CLR(sockfd, &allset); 107 client[i] = -1; 108 } 109 110 if (--nready <= 0) 111 break; 112 } 113 } 114 } 115 } 116 117 118 bool handle_recv(int connfd) { 119 120 char recvbuf[BUFSIZE]; 121 122 memset( recvbuf, '\0', BUFSIZE ); 123 if ( recv(connfd, recvbuf,BUFSIZE,0) != 0) { 124 if (!strcmp(recvbuf, "exit")) 125 return true; 126 fprintf(stderr,"recv msg: \"%s\" from connfd:%d\n", recvbuf, connfd); 127 send(connfd, recvbuf, strlen(recvbuf), 0); 128 fprintf(stderr,"send back: \"%s\" to connfd:%d\n\n", recvbuf, connfd); 129 } 130 else 131 return true; 132 return false; 133 }
客户端:
#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版)》