TCP/IP詳解--接收RST迴應的幾種情況
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
出現RST分節的情況可以在三種情況下發生,在連線建立時、在中間傳送資料時、在連線關閉時。
連線建立時出現RST迴應的情況有:傳送一個不存在的埠,想一個Listen的套接字或者埠上傳送資料分節(不是建立連線的分節)
傳送資料時出現RST分節的情況有:資料出現錯誤、不是按照seq要求來發送資料時
連線終止時傳送RST分節的情況有:如果在關閉的時候設定了套接字選項,l_linger,那麼在關閉的時候可能會出現RST分節
在TCP協議中RST表示復位,用來異常的關閉連線,在TCP的設計中它是不可或缺的。傳送RST包關閉連線時,不必等緩衝區的包都發出去,直接就丟棄快取區的包傳送RST包。而接收端收到RST包後,也不必傳送ACK包來確認。
其實在網路程式設計過程中,各種RST錯誤其實是比較難排查和找到原因的。下面我列出幾種會出現RST的情況。
1 埠未開啟
伺服器程式埠未開啟而客戶端來連線。這種情況是最為常見和好理解的一種了。去telnet一個未開啟的TCP的埠可能會出現這種錯誤。這個和作業系統的實現有關。在某些情況下,作業系統也會完全不理會這些發到未開啟埠請求。
比如在下面這種情況下,主機241向主機114傳送一個SYN請求,表示想要連線主機114的40000埠,但是主機114上根本沒有開啟40000這個埠,於是就向主機241傳送了一個RST。這種情況很常見。特別是伺服器程式core dump之後重啟之前連續出現RST的情況會經常發生。
當然在某些作業系統的主機上,未必是這樣的表現。比如向一臺WINDOWS7的主機發送一個連線不存在的埠的請求,這臺主機就不會迴應。
2 請求超時
曾經遇到過這樣一個情況:一個客戶端連線伺服器,connect返回-1並且error=EINPROGRESS。 直接telnet發現網路連線沒有問題。ping沒有出現丟包。用抓包工具檢視,客戶端是在收到伺服器發出的SYN之後就莫名其妙的傳送了RST。
比如像下面這樣:
有89、27兩臺主機。主機89向主機27傳送了一個SYN,表示希望連線8888埠,主機27迴應了主機89一個SYN表示可以連線。但是主機27卻很不友好,莫名其妙的傳送了一個RST表示我不想連線你了。
後來經過排查發現,在主機89上的程式在建立了socket之後,用setsockopt的SO_RCVTIMEO選項設定了recv的超時時間為100ms。而我們看上面的抓包結果表示,從主機89發出SYN到接收SYN的時間多達110ms。(從15:01:27.799961到15:01:27.961886, 小數點之後的單位是微秒)。因此主機89上的程式認為接收超時,所以傳送了RST拒絕進一步傳送資料。
3 提前關閉
關於TCP,我想我們在教科書裡都讀到過一句話,'TCP是一種可靠的連線'。 而這可靠有這樣一種含義,那就是作業系統接收到的來自TCP連線中的每一個位元組,我都會讓應用程式接收到。如果應用程式不接收怎麼辦?你猜對了,RST。
看兩段程式:
//server.cint main(int argc, char** argv) { int listen_fd, real_fd; struct sockaddr_in listen_addr, client_addr; socklen_t len = sizeof(struct sockaddr_in); listen_fd = socket(AF_INET, SOCK_STREAM, 0); if(listen_fd == -1) { perror("socket failed "); return -1; } bzero(&listen_addr,sizeof(listen_addr)); listen_addr.sin_family = AF_INET; listen_addr.sin_addr.s_addr = htonl(INADDR_ANY); listen_addr.sin_port = htons(SERV_PORT); bind(listen_fd,(struct sockaddr *)&listen_addr, len); listen(listen_fd, WAIT_COUNT); while(1) { real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len); if(real_fd == -1) { perror("accpet fail "); return -1; } if(fork() == 0) { close(listen_fd); char pcContent[4096]; read(real_fd,pcContent,4096); close(real_fd); exit(0); } close(real_fd); } return 0; }
|
這一段是server的最簡單的程式碼。邏輯很簡單,監聽一個TCP埠然後當有客戶端來連線的時候fork一個子程序來處理。注意看的是這一段fork裡面的處理:
char pcContent[4096];read(real_fd,pcContent,4096);close(real_fd);
|
然後再看一下client的程式碼:
//client.cint main(int argc, char** argv) { int send_sk; struct sockaddr_in s_addr; socklen_t len = sizeof(s_addr); send_sk = socket(AF_INET, SOCK_STREAM, 0); if(send_sk == -1) { perror("socket failed "); return -1; } bzero(&s_addr, sizeof(s_addr)); s_addr.sin_family = AF_INET; inet_pton(AF_INET,SER_IP,&s_addr.sin_addr); s_addr.sin_port = htons(SER_PORT); if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1) { perror("connect fail "); return -1; } char pcContent[5000]={0}; write(send_sk,pcContent,5000); sleep(1); close(send_sk);}
|
前三行就是TCP的3次握手,從第四行開始看,客戶端的49660埠向伺服器的9877埠傳送了5000個位元組的資料,然後伺服器端傳送了一個ACK進行了確認,緊接著伺服器向客戶端傳送了一個RST斷開了連線。和我們的預期一致。
4 在一個已關閉的socket上收到資料
如果某個socket已經關閉,但依然收到資料也會產生RST。
程式碼如下:
客戶端:
int main(int argc, char** argv) { int send_sk; struct sockaddr_in s_addr; socklen_t len = sizeof(s_addr); send_sk = socket(AF_INET, SOCK_STREAM, 0); if(send_sk == -1) { perror("socket failed "); return -1; } bzero(&s_addr, sizeof(s_addr)); s_addr.sin_family = AF_INET; inet_pton(AF_INET,SER_IP,&s_addr.sin_addr); s_addr.sin_port = htons(SER_PORT); if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1) { perror("connect fail "); return -1; } char pcContent[4096]={0}; write(send_sk,pcContent,4096); sleep(1); write(send_sk,pcContent,4096); close(send_sk);}
|
服務端:
int main(int argc, char** argv) { int listen_fd, real_fd; struct sockaddr_in listen_addr, client_addr; socklen_t len = sizeof(struct sockaddr_in); listen_fd = socket(AF_INET, SOCK_STREAM, 0); if(listen_fd == -1) { perror("socket failed "); return -1; } bzero(&listen_addr,sizeof(listen_addr)); listen_addr.sin_family = AF_INET; listen_addr.sin_addr.s_addr = htonl(INADDR_ANY); listen_addr.sin_port = htons(SERV_PORT); bind(listen_fd,(struct sockaddr *)&listen_addr, len); listen(listen_fd, WAIT_COUNT); while(1) { real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len); if(real_fd == -1) { perror("accpet fail "); return -1; } if(fork() == 0) { close(listen_fd); char pcContent[4096]; read(real_fd,pcContent,4096); close(real_fd); exit(0); } close(real_fd); } return 0; }
|
5、異常終止一個連線
終止一個連線的正常方式是一方傳送F I N。有時這也稱為有序釋放(orderly release),因為在所有排隊資料都已傳送之後才傳送F I N,正常情況下沒有任何資料丟失。但也有可能傳送一個復位報文段(RST)而不是F I N來中途釋放一個連線。有時稱這為異常釋放。
異常終止一個連線對應用程式來說有兩個優點:
(1)丟棄任何待發資料並立即傳送復位報文段;
(2)R S T的接收方會區分另一端執行的是異常關閉還是正常關閉。
應用程式使用的A P I必須提供產生異常關閉而不是正常關閉的手段
6、向處於LISTEN的埠傳送資料
如果殺掉伺服器端處理客戶端的子程序,程序退出後,關閉它開啟的所有檔案描述符,此時,當伺服器TCP接收到來自此客戶端的資料時,由於先前開啟的那個套接字介面的程序已終止,所以以RST響應。
父程序:192.168.0.80:5800*.*LISTENING
子程序:192.168.0.80:5800192.201.0.75:6500ESTALBING
殺掉子程序後,客戶端向伺服器端(192.168.0.80:5800 192.201.0.75:6500)傳送資料,則tcp_input呼叫in_pcblookup查詢對應的協議控制塊時,會找到伺服器端的父程序的協議控制塊,tcp_input繼續處理,在TCP_LISTEN狀態收到資料的處理方式如下:
n 如果報文段中帶有RST標誌,則丟棄它。
n 如果帶有ACK標誌,則丟棄並以RST作為響應。(本例正是這種情況)
n 如果沒帶有SYN,也沒有ACK標誌,則丟棄它。
7、連線沒有服務程序的埠號
在客戶端連線伺服器時,如果該伺服器主機在制定的埠上沒有程序在等待與之連線(例如伺服器程序也許沒有執行),那麼客戶端接收到RST迴應,客戶一接收到RST就馬上返回ECONNERFUSED錯誤。