一、非堵塞讀與寫分析
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首部從協議棧往下傳遞