【反思】一個價值兩天的BUG,無論工作還是學習C語言的朋友都看看吧!


 

博文原創,轉載請聯系博主!

 

使用C語言也有兩個年頭了,BUG寫出來過不少,也改過不少BUG。但是偏偏就是有這么一個BUG讓我手頭的項目停工了兩天,原因從百度找到谷歌,資料從MAN手冊找到RFC也沒有找到問題的原因,但是真正發現BUG原因之后實在是讓自己汗顏。

不管如何,決定把這個BUG寫進博文,也是給學習C語言的朋友們提個醒,查看BUG的眼光不要太高,思考問題要自底向上思考。

 

具體項目在我的github里:  https://github.com/yue9944882/HttpAccelerater

 

 

正文:

 

問題大致發現是這樣的,在這個HTTP下載器中,實質上編程邏輯都是在傳輸層TCP套接字上完成的,具體細節就不多提,在通訊過程中是通過一對send和recv函數完成的,recv函數原型如下所示:(linux環境<sys/socket.h>)

 

int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);

 

sockfd:   接收端套接字描述符

buff:     用來存放recv函數接收到的數據的緩沖區

nbytes:   指明buff的長度

flags:     一般置為0

返回值:  recv函數返回其實際copy的字節數

 
flags 說明 recv send
 MSG_DONTROUTE 繞過路由表查找      •
 MSG_DONTWAIT 僅本操作非阻塞    •       •
 MSG_OOB     發送或接收帶外數據   •   •
 MSG_PEEK   窺看外來消息   •  
 MSG_WAITALL   等待所有數據    •  

 

  問題就是出在recv函數的返回值這里,因為和HTTP服務器的通訊過程中,服務器端所返回的內容不僅僅是包括一個所請求的文件數據,還有http的報頭,而且在recv得到的數據中HTTP首部和實體是混合在一起的,所以就需要我們用\r\n\r\n四個字符作為標志來檢測HTTP首部的結束,而且又因為recv得到的數據並不是完整地填充進接收數據的緩沖區中的,所以我們計算接收到的文件的第一段數據的偏移是這樣的:

 

[ HTTP首部結束的偏移,實際讀進緩沖區的偏移 ]

 

因為HTTP首部長度遠遠小於緩沖區長度就忽略首部填滿緩沖區的情況。

 

recv函數是得到實際讀進緩沖區偏移的關鍵,可是實際調試過程中每次recv返回的值都是0!

可是緩沖區里面卻讀進了請求的數據,里面也有完整的HTTP首部,這究竟是為什么呢?

 

  那么我們查看一下recv函數返回0的具體原因:

 

recv() returns 0 only when you request a 0-byte buffer or the other peer has gracefully disconnected.

 

  首先我們的緩沖區確確實實寫進了數據,就談不上0-byte buffer,另一種情況就是TCP連接的正常關閉,即服務器端發送FIN包,但是如下所示,我們的代碼中有后續的while循環仍然接受到了服務器端傳送的數據,代碼如下所示:

 

    while(curPos<gURLinfo.llContentLen){
        dr=recv(sockdesc,recvBuf,4096,0);
        if(dr+curPos>gURLinfo.llContentLen){
            dw=pwrite(file,recvBuf,gURLinfo.llContentLen-headlength-curPos,curPos);    
            curPos+=dw;
            //printf("offset:\t%d\ndw:\t%d\n",curPos,dw);
            break;
        }else{
            dw=pwrite(file,recvBuf,dr,curPos);
            curPos+=dw;
        }
        //printf("offset:\t%d\ndw:\t%d\n",curPos,dw);
    }

 

  在這里recv返回的dr值竟然是正常的非零正值--從內核讀取進緩沖區的字節數! 那么我們自然就會開始認為是recv函數的第一次使用才會返回0,之后的使用不會再出現問題。於是我就用了一個“弄巧成拙的辦法”:首先使用bzero函數將緩沖區填充滿0,再在緩沖區中尋找以‘\0’為結束標志進行掃描,掃描結束的時候得到緩沖區內實際字節的長度。但是實際測試的時候發現,這個辦法用於下載純粹的txt格式的文件是沒有問題的,然而當下載二進制文件例如圖片,壓縮后文件的時候,就會出現問題!ps:這樣下載下來的圖片竟然還是偏紅的,害得我去圖片編碼區RBG值尋找BUG真相,浪費了很多時間。

  既然總是返回0,我們來查看一下是否是有錯誤發生吧,於是加入了<errno.h>,結果輸出出來了errno還是0,也就是無錯誤發生!

 

 

最終這個問題的解決過程是這樣的:

 

1.使用wget下載完整的圖片。

2.使用 vim -b 圖片 和正常的圖片進行對比(:%!xxd 查看),發現和正常圖片不同之處,在於一些空白0字段的填充

3.進而發現還是第一次recv函數導致的文件內容不對

4.最終問題鎖定在了這樣一段代碼,也是讓我最汗顏的:

 

    if(dr=recv(sockdesc,recvBuf,4096,0)==-1){
        fprintf(stderr,"Header Recving Failure!\n");
        exit(-1);
    }

 

這是recv函數第一次接收讀取字節數的代碼,也就是這段代碼導致了recv讀取字節數的不可知,返回值永遠為0。相信C語言的老手已經看出來了,這段代碼的問題:

 

關系運算符優先級大於賦值運算符!!!

 

真正的正確的代碼應該是這樣寫的:

 

    if((dr=recv(sockdesc,recvBuf,4096,0))==-1){
        fprintf(stderr,"Header Recving Failure!\n");
        exit(-1);
    }

 

C語言寫多了,有些代碼會越寫越簡練,比如聲明和運算混寫,函數參數局部全局混寫,但是對於關系運算符的優先級是最不能忽略的,無論是哪個語言,哪怕是運算符關系符最混亂的perl,也要牢牢記住每個優先級和結合性!

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM