非堵塞IO的讀與寫的回射客戶端對比分析

一、非堵塞讀與寫分析

1、老版本1分析--- 基於tcp的堵塞io模型

是read和write,結合起來處理的io模型,因爲如果服務器沒有數據過來,那麼read將會一直堵塞下去。因爲堵塞將會導致程序的效率不夠高效。

僞代碼如下如實:

while(fgets(buf, maxline,stdin) != NULL){
	write(sockfd, buf, strlen(buf));
	read(sockfd, buf, maxline);
}

分析堵塞條件:

1).write當中如果套接字發送緩衝區已滿,write調用將會堵塞

2).read當中如果套接字接受緩衝區沒有數據, 那麼read將會一直堵塞下去,直到收到了來之服務器的數據。


2、老版本2分析---基於tcp的select中堵塞的io複用模型

在select來判斷, 是否能讀取數據, 可以排除老版本一當中的讀取數據時候而造成的程序堵塞情況, 很大程度的提高了程序的執行效率。

僞代碼如下所示:

for(;;){
	FD_SET(fileno(stdin), &rset);
	FD_SET(sockfd, &rset);
	
	select(maxfd + 1, &rset, NULL, NULL, NULL);
	if(FD_ISSET(fileno(stdin), &rset)){
		相關處理過程
	}
	
	if(FD_ISSET(sockfd, &rset)){
		相關處理過程 
	}
	
}
分析堵塞條件:
1).然而當調用write函數的時候,也會應爲套接字發送緩衝區已滿,而導致堵塞。

2).如果標準輸出比網絡慢, 那麼進程也有可能堵塞與後續的write調用。


3、非堵塞IO的讀與寫在select下的複用模型

我們需要在應用層維護兩個緩衝區:to緩衝區是標準輸入到服務器去的數據, fr緩衝區是容納來自服務器到標準輸出的數據

to緩衝區:tooptr指向緩衝區未發送到服務器的數據的頭子節。toiptr指向標準輸入讀入的數據可以存放的下一個字節。有(toiptr - tooptr)個數據可以被髮送到套接字。

可以從標準輸入讀入的字節數&to[MAXLINE] - toiptr。當tooptr = toiptr的時候, 這兩個指針就恢復到緩衝區開始的位置。

fr緩衝區與to緩衝區有相同的道理。


模型圖如下所示:


詳細實現代碼如下所示:

dg_cli_nonblockio.c

#include "unp.h"

void str_cli(FILE *fp, int sockfd){
    int maxfdp1, flag, stdineof;
    ssize_t n, nwritten;
    fd_set wset, rset;
    char to[MAXLINE], fr[MAXLINE];
    char *toiptr, *tooptr, *friptr, *froptr;
    //重點一:把標準輸入、標準輸出、連接服務器的套接字都設置成非堵塞條件 
    flag = Fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, flag | O_NONBLOCK);

    flag = Fcntl(STDIN_FILENO, F_GETFL, 0);
    Fcntl(STDIN_FILENO, F_GETFL, flag | O_NONBLOCK);

    flag = Fcntl(STDOUT_FILENO, F_GETFL, 0);
    Fcntl(STDOUT_FILENO, F_SETFL, flag | O_NONBLOCK);

    
    toiptr = tooptr = to;
    friptr = froptr = fr;
    stdineof = 0;
    maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd);
    for(;;){

        FD_ZERO(&rset);
        FD_ZERO(&wset);
        //標準輸出未讀到eof的時候, 並且to的空閒緩衝區至少在一個字節字節以上, 打開讀描述符集中的標準輸入位 
        if(stdineof == 0 && toiptr < &to[MAXLINE]){  
           FD_SET(STDIN_FILENO, &rset);    /* read from stdin*/
        }
        //當fr緩衝區有空閒時候, 則設置描述符集中的標準輸出位 
        if(friptr < &fr[MAXLINE]){
            FD_SET(sockfd, &rset);  /*read from socket*/
        }
        //tooptr一直小於等於toiptr的值,當不等於的時候,則表示to緩衝區有數據可以輸出到socket 
        if(tooptr != toiptr){
            FD_SET(sockfd, &wset);  /*write to socket*/
        }
		// froptr一直小於等於friptr的值,當不等於的時候,則表示fr緩衝區有數據可以標準輸出 
        if(froptr != friptr){
           FD_SET(STDOUT_FILENO, &wset);   /* write to stdout*/
        }

        Select(maxfdp1, &rset, &wset, NULL, NULL);

        if(FD_ISSET(STDIN_FILENO, &rset)){
            if((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0){
                if(errno != EWOULDBLOCK){  //當讀取失敗的時候有情況,第一非堵塞條件下沒有成功讀取數據, 
                                           //errno置爲EWOULDBLOCK,這種錯誤可以忽略。另一種情況就是讀取錯誤
					fprintf(stdout, "read error from stdin\n");
                }
            }else if(n == 0){//read返回爲0,標準輸入結束,設置stdineof標誌。當to緩衝區不在有數據發送的時候 
                stdineof = 1;//那麼發送FIN分節, 如果有數據發送,則推遲FIN分節的發送 
                fprintf(stderr, "%s, EOF on stdin\n", gf_time());   /* gf_time() is a function that we finished by  ourselves.*/
                if(toiptr == tooptr)   Shutdown(sockfd, SHUT_WR);   /* send FIN*/
            }else{
                toiptr += n;
                fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);    /*just read*/
                FD_SET(sockfd, &wset);          //try and write to sockfd below
            }
        }

        if(FD_ISSET(sockfd, &rset)){
            if((n = read(sockfd, friptr, (size_t)(&fr[MAXLINE] - friptr))) < 0){
                if(errno != EWOULDBLOCK)
                    fprintf(stderr, "read error from sockfd\n");/*read error from socket*/

            }else if(n == 0){
                fprintf(stderr, "%s:EOF on sockfd\n", gf_time());
                if(stdineof == 1) return;           /*normal termination*/
            }else{
                fprintf(stderr, "%s: read %d bytes from sockfd\n", gf_time(), n);    /*just read*/
                friptr += n;
                FD_SET(STDOUT_FILENO, &wset);       /*try and write below*/
            }
        }
        //標準輸出的條件是標準輸出描述符號可寫,並且fr緩衝區有數據可以輸出 
        if(FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0 )){ // 判斷的同時並且也獲得了fr緩衝區中爲數據的bytes大小 
            if((nwritten = write(STDOUT_FILENO, friptr, n)) < 0){//輸出失敗時候,EWOULDBLOCK錯誤忽略 
                if(errno != EWOULDBLOCK) fprintf(stderr, "write error to stdout\n");
            }else{
                fprintf(stderr, "%s: wrote %d bytes to stdout\n", gf_time(), nwritten);
                friptr += nwritten;         /**just written*/
                //當輸出指針(froptr)追上輸入指針(friptr)的時候,指向fr緩衝區開始位置 
                if(friptr == froptr)    friptr = froptr = fr;   /*back to begining of buffer*/
            }
        }

        if(FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)){
            if((nwritten = write(sockfd, friptr, n)) < 0){
                if(errno != EWOULDBLOCK) fprintf(stderr, "write error to socket\n");
            }else{
                fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten);
                toiptr += nwritten;         /*just written*/
                if(toiptr == tooptr){    
					toiptr = tooptr = to;   /*back to begining of buffer*/
                	if(stdineof) Shutdown(sockfd, SHUT_WR);  /* send FIN */
                	//當stdin已經關閉, 並且to緩衝的數據都已經發送到了服務器, 那麼接發送FIN分節到服務器。 
                } 
            }
        }
    }
}
詳細分析如代碼中的中文註釋可得:

二、非堵塞IO問題分析

套接字默認狀態是堵塞的。這也就說當我們發送出一個io請求的時候,如果不能及時的完成,那麼進程將會進入睡眠狀態,等待操作的進行。

(1)、輸入操作,包括read, readv,recv,recvmsg,recvfrom五個函數。非堵塞的情況下如果不能被滿足(對於tcp至少有一個數據可讀,對於UDP套接字即有一個完整的數據報可讀),相應的調用返回-1,並且errno將會返回EWOULDBLOCK錯誤

(2)、輸出操作,包括write,writev、send、sendmsg、sendto五個函數。對與tcp而言,在堵塞套接字中,內核將會把數據從應用層緩衝區的數據複製到套接字緩衝區,如果套接字緩衝區空間不夠哪兒內核將會進入休眠的狀態,在非堵塞套接字中,如果套接字緩衝區的空間沒有,那麼將會立刻返回EWOULDBLOCK錯誤,若是有部分空間,則是返回該部分空間的字節數。對於UDP而言, UDP套接字不存在正真的緩衝區,內核直接將數據從應用層緩衝區換衣UDP首部和IP首部從協議棧往下傳遞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章