使用libevent編寫websocket服務端時遇到了一個問題:
瀏覽器與服務端建立連接后,若刷新或關閉瀏覽器窗口(未監聽相應事件並處理),服務端無法得知連接斷開,按理說,這種情況屬於client異常終止,跟拔網線的情況類似。這種情況下,服務端不知情,仍保留此連接,仍按照既定邏輯向client寫數據,寫了兩次后,服務端程序終止(多次測試,均是第2次后終止),不是崩潰,是異常終止,非常不解。
問了一位很有經驗的同事,得知是SIGPIPE信號導致程序終止。
查了相關資料,大致明白:連接建立,若某一端關閉連接,而另一端仍然向它寫數據,第一次寫數據后會收到RST響應,此后再寫數據,內核將向進程發出SIGPIPE信號,通知進程此連接已經斷開。而SIGPIPE信號的默認處理是終止程序,導致上述問題的發生。
為避免這種情況,可以選擇忽略SIGPIPE信號,不執行任何動作。
#include <signal.h>
//SIGPIPE ignore
struct sigaction act;
act.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &act, NULL) == 0) {
LOG("SIGPIPE ignore");
}
最近為測試自己寫的服務器,臨時寫了一個客戶端,總是發現客戶端收到SIGPIPE的信號,然后進程退出。
為了客戶端進程收到SIGPIPE不退出,我打算忽略該信號,下面是我用過的方法:
(1)間接忽略
- static void SignalHandler(int nSigno)
- {
- signal(nSigno, SignalHandler);
- switch(nSigno)
- {
- case SIGPIPE:
- printf("Process will not exit\n");
- break;
- default:
- printf("%d signal unregister\n", nSigno);
- break;
- }
- }
- atic void InitSignalHandler()
- {
- signal(SIGPIPE , &SignalHandler);
- }
- int main()
- {
- InitSignalHandler();
- ........
- return 0;
- }
(2)直接忽略
- signal(SIGPIPE,SIG_IGN);
(3)重載signaction
- struct sigaction sa;
- sa.sa_handler = SIG_IGN;
- sigaction( SIGPIPE, &sa, 0 );
於是乎,查SIGPIPE這個信號的特性:
如果在寫到管道時讀進程已終止,則產生此信號。當類型為SOCK_STREAM的套接字已不再連接時,進程寫到該套接字也產生此信號。
——《UNIX環境高級編程》中10.2節
由於我的客戶端是用send()進行發送數據的,通過man手冊查看send()函數,看到有一條這樣說:
MSG_NOSIGNAL
Requests not to send SIGPIPE on errors on stream oriented sockets when the other end breaks the connection. The EPIPE error is still returned.
於是將send()最后一個參數flags改為MSG_NOSIGNAL,再次啟動客戶端測試。SIGPIPE被忽略,客戶端沒有因為該信號退出。
將這次經歷記錄下來,與看到的人共享。
當服務器close一個連接時,若client端接着發數據。根據TCP協議的規定,會收到一個RST響應,client再往這個服務器發送數據時,系統會發出一個SIGPIPE信號給進程,告訴進程這個連接已經斷開了,不要再寫了。
根據信號的默認處理規則SIGPIPE信號的默認執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把SIGPIPE設為SIG_IGN
如: signal(SIGPIPE,SIG_IGN);
這時SIGPIPE交給了系統處理。
服務器采用了fork的話,要收集垃圾進程,防止僵屍進程的產生,可以這樣處理:
signal(SIGCHLD,SIG_IGN); 交給系統init去回收。
這里子進程就不會產生僵屍進程了。
我寫了一個服務器程序,在Linux下測試,然后用C++寫了客戶端用千萬級別數量的短鏈接進行壓力測試. 但是服務器總是莫名退出,沒有core文件.
最后問題確定為, 對一個對端已經關閉的socket調用兩次write, 第二次將會生成SIGPIPE信號, 該信號默認結束進程.
具體的分析可以結合TCP的"四次握手"關閉. TCP是全雙工的信道, 可以看作兩條單工信道, TCP連接兩端的兩個端點各負責一條. 當對端調用close時, 雖然本意是關閉整個兩條信道, 但本端只是收到FIN包. 按照TCP協議的語義, 表示對端只是關閉了其所負責的那一條單工信道, 仍然可以繼續接收數據. 也就是說, 因為TCP協議的限制, 一個端點無法獲知對端的socket是調用了close還是shutdown.
對一個已經收到FIN包的socket調用read方法, 如果接收緩沖已空, 則返回0, 這就是常說的表示連接關閉. 但第一次對其調用write方法時, 如果發送緩沖沒問題, 會返回正確寫入(發送). 但發送的報文會導致對端發送RST報文, 因為對端的socket已經調用了close, 完全關閉, 既不發送, 也不接收數據. 所以, 第二次調用write方法(假設在收到RST之后), 會生成SIGPIPE信號, 導致進程退出.
為了避免進程退出, 可以捕獲SIGPIPE信號, 或者忽略它, 給它設置SIG_IGN信號處理函數:
signal(SIGPIPE, SIG_IGN);
這樣, 第二次調用write方法時, 會返回-1, 同時errno置為SIGPIPE. 程序便能知道對端已經關閉.
在linux下寫socket的程序的時候,如果嘗試send到一個disconnected socket上,就會讓底層拋出一個SIGPIPE信號。
這個信號的缺省處理方法是退出進程,大多數時候這都不是我們期望的。因此我們需要重載這個信號的處理方法。調用以下代碼,即可安全的屏蔽SIGPIPE:
signal (SIGPIPE, SIG_IGN);
我的程序產生這個信號的原因是:
client端通過 pipe 發送信息到server端后,就關閉client端, 這時server端,返回信息給 client 端時就產生Broken pipe 信號了,服務器就會被系統結束了。
對於產生信號,我們可以在產生信號前利用方法 signal(int signum, sighandler_t handler) 設置信號的處理。如果沒有調用此方法,系統就會調用默認處理方法:中止程序,顯示提示信息(就是我們經常遇到的問題)。我們可以調用系統的處理方法,也可以自定義處理方法。
系統里邊定義了三種處理方法:
(1)SIG_DFL信號專用的默認動作:
(a)如果默認動作是暫停線程,則該線程的執行被暫時掛起。當線程暫停期間,發送給線程的任何附加信號都不交付,直到該線程開始執行,但是SIGKILL除外。
(b)把掛起信號的信號動作設置成SIG_DFL,且其默認動作是忽略信號 (SIGCHLD)。
(2)SIG_IGN忽略信號
(a)該信號的交付對線程沒有影響
(b)系統不允許把SIGKILL或SIGTOP信號的動作設置為SIG_DFL
3)SIG_ERR
項目中我調用了signal(SIGPIPE, SIG_IGN), 這樣產生 SIGPIPE 信號時就不會中止程序,直接把這個信號忽略掉。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
在編寫一個仿QQ軟件,C/S模式。出現的問題:當客戶機關閉時,服務器也隨着關閉,糾結很久之后,我gdb了下,出現下面提示信息:
Program received signal SIGPIPE, Broken pipe.
0x0012e416 in __kernel_vsyscall ()
在 網上查了一下出現SIGPIPE的原因:如果嘗試send到一個已關閉的 socket上兩次,就會出現此信號,也就是用協議TCP的socket編程,服務器是不能知道客戶機什么時候已經關閉了socket,導致還在向該已關 閉的socket上send,導致SIGPIPE。
而系統默認產生SIGPIPE信號的措施是關閉進程,所以出現了服務器也退出。
下面分析TCP協議的缺陷以至於服務器無法及時判斷對方socket已關閉:
具 體的分析可以結合TCP的"四次握手"關閉. TCP是全雙工的信道, 可以看作兩條單工信道, TCP連接兩端的兩個端點各負責一條. 當對端調用close時, 雖然本意是關閉整個兩條信道, 但本端只是收到FIN包. 按照TCP協議的語義, 表示對端只是關閉了其所負責的那一條單工信道, 仍然可以繼續接收數據. 也就是說, 因為TCP協議的限制, 一個端點無法獲知對端的socket是調用了close還是shutdown.(此段網上抄來的)
解決方法:
重新定義遇到SIGPIPE的措施,signal(SIGPIPE, SIG_IGN);具體措施在函數SIG_IGN里面寫。
摘自:
當服務器close一個連接時,若client端接着發數據。根據TCP協議的規定,會收到一個RST響應,client再往這個服務器發送數據時,系統會發出一個SIGPIPE信號給進程,告訴進程這個連接已經斷開了,不要再寫了。
又或者當一個進程向某個已經收到RST的socket執行寫操作是,內核向該進程發送一個SIGPIPE信號。該信號的缺省學位是終止進程,因此進程必須捕獲它以免不情願的被終止。
根據信號的默認處理規則SIGPIPE信號的默認執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把 SIGPIPE設為SIG_IGN
如:signal(SIGPIPE, SIG_IGN);
這時SIGPIPE交給了系統處理。
服務器采用了fork的話,要收集垃圾進程,防止僵屍進程的產生,可以這樣處理:
signal(SIGCHLD,SIG_IGN);
交給系統init去回收。
這里子進程就不會產生僵屍進程了。
在linux下寫socket的程序的時候,如果嘗試send到一個disconnected socket上,就會讓底層拋出一個SIGPIPE信號。
這個信號的缺省處理方法是退出進程,大多數時候這都不是我們期望的。因此我們需要重載這個信號的處理方法。調用以下代碼,即可安全的屏蔽SIGPIPE:
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );
signal設置的信號句柄只能起一次作用,信號被捕獲一次后,信號句柄就會被還原成默認值了。
sigaction設置的信號句柄,可以一直有效,值到你再次改變它的設置。
struct sigaction action;
action.sa_handler = handle_pipe;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGPIPE, &action, NULL);
void handle_pipe(int sig)
{
//不做任何處理即可
}
RST的含義為“復位”,它是TCP在某些錯誤情況下所發出的一種TCP分節。有三個條件可以產生RST:
1), SYN到達某端口但此端口上沒有正在監聽的服務器。
2), TCP想取消一個已有連接
3), TCP接收了一個根本不存在的連接上的分節。
1. Connect 函數返回錯誤ECONNREFUSED:
如果對客戶的SYN的響應是RST,則表明該服務器主機在我們指定的端口上沒有進程在等待與之連接(例如服務器進程也許沒有啟動),這稱為硬錯(hard error),客戶一接收到RST,馬上就返回錯誤ECONNREFUSED.
TCP為監聽套接口維護兩個隊列。兩個隊列之和不超過listen函數第二個參數backlog。
當一個客戶SYN到達時,若兩個隊列都是滿的,TCP就忽略此分節,且不發送RST.這個因為:這種情況是暫時的,客戶TCP將重發SYN,期望不久就能 在隊列中找到空閑條目。要是TCP服務器發送了一個RST,客戶connect函數將立即發送一個錯誤,強制應用進程處理這種情況,而不是讓TCP正常的 重傳機制來處理。還有,客戶區別不了這兩種情況:作為SYN的響應,意為“此端口上沒有服務器”的RST和意為“有服務器在此端口上但其隊列滿”的 RST.
Posix.1g允許以下兩種處理方法:忽略新的SYN,或為此SYN響應一個RST.歷史上,所有源自Berkeley的實現都是忽略新的SYN。
2.如果殺掉服務器端處理客戶端的子進程,進程退出后,關閉它打開的所有文件描述符,此時,當服務器TCP接收到來自此客戶端的數據時,由於先前打開的那個套接字接口的進程已終止,所以以RST響應。
經常遇到的問題:
如果不判斷read , write函數的返回值,就不知道服務器是否響應了RST, 此時客戶端如果向接收了RST的套接口進行寫操作時,內核給該進程發一個SIGPIPE信號。此信號的缺省行為就是終止進程,所以,進程必須捕獲它以免不情願地被終止。
進程不論是捕獲了該信號並從其信號處理程序返回,還是不理會該信號,寫操作都返回EPIPE錯誤。
3. 服務器主機崩潰后重啟
如果服務器主機與客戶端建立連接后崩潰,如果此時,客戶端向服務器發送數據,而服務器已經崩潰不能響應客戶端ACK,客戶TCP將持續重傳數據分節,試圖從服務器上接收一個ACK,如果服務器一直崩潰客戶端會發現服務器已經崩潰或目的地不可達,但可能需要比較長的時間; 如果服務器在客戶端發現崩潰前重啟,服務器的TCP丟失了崩潰前的所有連接信息,所以服務器TCP對接收的客戶數據分節以RST響應。
二、關於socket的recv:
對於TCP non-blocking socket, recv返回值== -1,但是errno == EAGAIN, 此時表示在執行recv時相應的socket buffer中沒有數據,應該繼續recv。
【If no messages are available at the socket and O_NONBLOCK is not set on the socket's file descriptor, recv() shall block until a message arrives. If no messages are available at the socket and O_NONBLOCK is set on the socket's file descriptor, recv() shall fail and set errno to [EAGAIN] or [EWOULDBLOCK].】
對於UDP recv 應該一直讀取直到recv()==-1 && errno==EAGAIN,表示buffer中數據包被全部讀取。
接收數據時常遇到Resource temporarily unavailable的提示,errno代碼為11(EAGAIN)。這表明你在非阻塞模式下調用了阻塞操作,在該操作沒有完成就返回這個錯誤,這個錯誤不會破壞socket的同步,不用管它,下次循環接着recv就可以。對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和 Windows上,EAGAIN的名字叫做EWOULDBLOCK。其實這算不上錯誤,只是一種異常而已。
|
外記:
accetp()是慢系統調用,在信號產生時會中斷其調用並將errno變量設置為EINTR,此時應重新調用accept()。
所以使用時應這樣:
|
signal 與 sigaction 區別:
signal函數每次設置具體的信號處理函數(非SIG_IGN)只能生效一次,每次在進程響應處理信號時,隨即將信號處理函數恢復為默認處理方式.所以如果想多次相同方式處理某個信號,通常的做法是,在響應函數開始,再次調用signal設置。
int sig_int(); //My signal handler
...
signal(SIGINT, sig_int);
...
int sig_int()
{
signal(SIGINT, sig_int);
....
}
這種代碼段的一個問題是:在信號發生之后到信號處理程序中調用s i g n a l函數之間有一個
時間窗口。在此段時間中,可能發生另一次中斷信號。第二個信號會造成執行默認動作,而對
中斷信號則是終止該進程。這種類型的程序段在大多數情況下會正常工作,使得我們認為它們
正確,而實際上卻並不是如此。
另一個問題是:在進程不希望某種信號發生時,它不能關閉該信號
sigaction:
1.在信號處理程序被調用時,系統建立的新信號屏蔽字會自動包括正被遞送的信號。因此保證了在處理一個
給定的信號時,如果這種信號再次發生,那么它會被阻塞到對前一個信號的處理結束為止
2.響應函數設置后就一直有效,不會重置
3.對除S I G A L R M以外的所有信號都企圖設置S A _ R E S TA RT標志,於是被這些信號中斷
的系統調用(read,write)都能自動再起動。不希望再起動由S I G A L R M信號中斷的系統調用的原因是希望對I / O操作可以設置時間限制。 所以希望能用相同方式處理信號的多次出現,最好用sigaction.信號只出現並處理一次,可以用signal