1. 程式人生 > >伺服器程式設計入門(11)TCP併發回射伺服器實現

伺服器程式設計入門(11)TCP併發回射伺服器實現

問題聚焦: 當客戶端阻塞於從標準輸入接收資料時,將讀取不到別的途徑發過來的必要資訊,如TCP發過來的FIN標誌。 因此,程序需要核心一旦發現程序指定的一個或多個IO條件就緒(即輸入已準備好被讀取,或者描述符已能承接更多的輸出),它就通知程序。 這個機制稱為I/O複用,這是由select, poll, epoll函式支援的。 編譯環境:     Ubuntu12.04  g++ 需求描述:
  1. 單程序,IO複用,實現多個連線同時監聽和收發資訊
  2. 當伺服器程序一終止,客戶就能馬上得到結果(select +shutdown實現)
  3. 當客戶端使用"exit"命令或者Cirl+C結束程序時,伺服器可以立即感應到,並關閉當前介面(select+close實現
來看一下實現後的執行效果:
步驟:
  1. 伺服器連線了第一個客戶,並收發訊息“hello world”
  2. 伺服器連線了第二個客戶,並收發訊息“hello select”
  3. 伺服器從第一個客戶收發訊息“hello world again”
  4. 伺服器從第二個客戶收發訊息“hello select again”
  5. 第二個客戶關閉連線
  6. 第一個客戶關閉連線
在瞭解select實現之前,先複習一下之前瞭解的IO模型 五種IO模型
  • 阻塞式IO
  • 非阻塞式IO
  • IO複用
  • 訊號驅動式IO
  • 非同步IO
對比:
一個輸入操作通常包括兩個不同的階段:
  • 等待資料準備好
  • 從核心向程序複製資料
對於TCP來說,這兩步分別為:
  • 等待資料從網路中到達,當所等待分組到達時,它被複制到核心中的某個緩衝區
  • 把資料從核心緩衝區複製到應用程序緩衝區
select 流程:
  1. select主要通過維護兩個陣列,來實現埠的輪詢:
  2. client[]陣列,記錄有哪些連線已經建立
  3. rset[]陣列,記錄有註冊哪些埠,需要監聽
  4. 當rset陣列中註冊的埠被啟用,這時將埠號放到client陣列中,稍後遍歷client[]陣列,處理連線上的資料
程式碼實現: 伺服器端:
#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版)》