2011-10-14 51 views
4

我正在開發一個套接字應用程序,它必須對網絡故障有效。SO_KEEPALIVE在write()的調用過程中不起作用?

應用程序有2個正在運行的線程,一個來自套接字(一個read()循環)的等待消息,另一個發送消息到套接字(一個write()循環)。

我目前正在嘗試使用SO_KEEPALIVE來處理網絡故障。 它工作正常,如果我只在read()上被阻塞。連接丟失後幾秒鐘(網絡電纜被移除),read()將失敗並顯示'連接超時'消息。但是,如果在網絡斷開連接之後(並且在超時結束之前)嘗試使用wrte(),write()和read()將永遠阻塞,而不會出錯。

這是一個剝離示例代碼,它將stdin/stdout指向套接字。它偵聽端口5656:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <pthread.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <netinet/tcp.h> 

int socket_fd; 

void error(const char *msg) { 
    perror(msg); 
    exit(1); 
} 

//Read from stdin and write to socket 
void* write_daemon (void* _arg) { 
    while (1) { 
     char c; 
     int ret = scanf("%c", &c); 
     if (ret <= 0) error("read from stdin"); 
     int ret2 = write(socket_fd, &c, sizeof(c)); 
     if (ret2 <= 0) error("write to socket"); 
    } 
    return NULL; 
} 

//Read from socket and write to stdout 
void* read_daemon (void* _arg) { 
    while (1) { 
     char c; 
     int ret = read(socket_fd, &c, sizeof(c)); 
     if (ret <= 0) error("read from socket"); 
     int ret2 = printf("%c", c); 
     if (ret2 <= 0) error("write to stdout"); 
    } 
    return NULL; 
} 


//Enable and configure KEEPALIVE - To detect network problems quickly 
void config_socket() { 
    int enable_no_delay = 1; 
    int enable_keep_alive = 1; 
    int keepalive_idle  =1; //Very short interval. Just for testing 
    int keepalive_count =1; 
    int keepalive_interval =1; 
    int result; 

    //=> http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/#setsockopt 
    result = setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, &enable_keep_alive, sizeof(int)); 
    if (result < 0) 
     error("SO_KEEPALIVE"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPIDLE, &keepalive_idle, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPIDLE"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPINTVL, &keepalive_interval, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPINTVL"); 

    result = setsockopt(socket_fd, SOL_TCP, TCP_KEEPCNT, &keepalive_count, sizeof(int)); 
    if (result < 0) 
     error("TCP_KEEPCNT"); 
} 

int main(int argc, char *argv[]) { 
    //Create Server socket, bound to port 5656 
    int listen_socket_fd; 
    int tr=1; 
    struct sockaddr_in serv_addr, cli_addr; 
    socklen_t clilen = sizeof(cli_addr); 
    pthread_t write_thread, read_thread; 

    listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0); 
    if (listen_socket_fd < 0) 
     error("socket()"); 

    if (setsockopt(listen_socket_fd,SOL_SOCKET,SO_REUSEADDR,&tr,sizeof(int)) < 0) 
     error("SO_REUSEADDR"); 

    bzero((char *) &serv_addr, sizeof(serv_addr)); 
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_addr.s_addr = INADDR_ANY; 
    serv_addr.sin_port = htons(5656); 
    if (bind(listen_socket_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 
     error("bind()"); 

    //Wait for client socket 
    listen(listen_socket_fd,5); 
    socket_fd = accept(listen_socket_fd, (struct sockaddr *) &cli_addr, &clilen); 
    config_socket(); 
    pthread_create(&write_thread, NULL, write_daemon, NULL); 
    pthread_create(&read_thread , NULL, read_daemon , NULL); 
    close(listen_socket_fd); 
    pthread_exit(NULL); 
} 

重現錯誤,使用telnet 5656. 如果一對夫婦OS秒後,連接丟失,除非我試着寫在終端的東西后就會退出。在這種情況下,它會永遠阻止。

所以,問題是:怎麼了?如何解決它?還有其他的選擇嗎?

謝謝!


我試過使用Wireshark來檢查網絡連接。如果我不調用write(),我可以看到正在發送的TCP保持活動包,並且連接在幾秒鐘後關閉。

相反,如果我嘗試寫(),它停止發送保活分組,並開始發送TCP重發,而不是(這似乎挺合我意)。問題是,重傳之間的時間間隔在每次失敗後都會變得越來越大,並且似乎永遠不會放棄並關閉套接字。

有沒有辦法設置最大重傳次數或類似的東西? 謝謝

回答

1

不知道如果別人會給你一個更好的選擇,但在我參與過的幾個項目中,我們遇到了非常相似的情況。

對於我們的解決方案是簡單地採取控制到你自己的手中,而不是依賴於底層的OS /驅動程序告訴你什麼時候連接死亡。如果您控制客戶端和服務器端,則可以引入自己的ping消息,這些消息在客戶端和服務器之間跳轉。這樣,您可以a)控制自己的連接超時,並b)輕鬆保存一條記錄,指出連接的健康狀況。

在最近的應用中,我們躲在這些ping請求的本身,以便儘可能真實的客戶端/服務器應用程序代碼而言,連接超時只是工作的通信庫中帶內控制消息。

+0

我喜歡它,但我正在實現一個現有協議的一側,它沒有任何方法來強制'ping'。 –

0

write_daemon(),要存儲的write()的返回值到ret2變量,但然後使用ret變量而不是一個套接字錯誤檢查,所以你永遠不會真正捕獲任何write()錯誤。

+0

謝謝!我修好了。不幸的是,它沒有解決問題。 –

2

我發現了TCP_USER_TIMEOUT套接字選項(rfc5482),如果在指定的時間間隔之後發送的數據沒有被確認,它將關閉連接。

它工作正常,我=)

//defined in include/uapi/linux/tcp.h (since Linux 2.6.37) 
#define TCP_USER_TIMEOUT 18 

int tcp_timeout  =10000; //10 seconds before aborting a write() 

result = setsockopt(socket_fd, SOL_TCP, TCP_USER_TIMEOUT, &tcp_timeout, sizeof(int)); 
if (result < 0) 
    error("TCP_USER_TIMEOUT"); 

不過,我覺得我不應該同時使用SO_KEEP_ALIVE和TCP_USER_TIMEOUT。 也許這是錯誤的地方?

+0

這不是一個錯誤,'TCP_USER_TIMEOUT'已經被實現來解決你上面描述的同樣的問題,即如果發送keepalive數據包的那一方,如果在那個套接字上發送一個數據包,那麼重傳定時器就會啓動並且在15-20分鐘後斷開連接!使用'TCP_USER_TIMEOUT',對數據包可以保持未確認狀態的時間有嚴格的限制。 – brokenfoot

1

TCP Keep Alive在RFC1122中指定。 TCP的Keep Alive特性不是檢測短期網絡中斷,而是清理可能使用寶貴資源的TCP控制塊/緩衝區。該RFC也是在1989年編寫的。RFC明確規定,TCP Keep Alive不得每兩小時發送一次以上,並且只有在沒有其他流量時才需要發送。如果更高級別的協議需要檢測連接丟失,則自行完成它是更高級協議的工作。 BGP路由協議在TCP上運行,默認情況下每60秒發送一次自己的Keep Alive消息形式。 BGP規範說,如果在最後3 * keep_alive_interval秒內沒有看到新的流量,則連接將被視爲死亡。 OpenSSH以乒乓球的形式實現它自己的活着。它會重試發送X預期在Y時間內響應(pong)的消息,或者它終止連接。面對臨時的網絡中斷,TCP本身非常難以交付數據,而且它本身並沒有用於檢測網絡中斷。通常情況下,如果要實現保持活動狀態並希望避免阻塞,可以切換到非阻塞I/O並維護一個可以與select()/ poll()調用一起使用的定時器。時間到。另一個選擇可能是使用單獨的計時器線程,或者甚至使用SIGALARM的更粗糙的方法。我建議使用帶fcntl()的O_NONBLOCK來將套接字設置爲非阻塞I/O。然後,您可以使用gettimeofday()記錄何時接收到傳入的I/O,並使用select()進行睡眠,直到下一個Keep Alive到期或發生I/O。

+0

不完全是你說的。 RFC 1122指出,第一個keepalive探針的DEFAULT時間間隔不應少於兩個小時。這意味着用戶可以將其設置爲任何想要的。 – lvella

1

在斷開電纜連接之前,您是否收到對方的字節或ACK?也許這是關係到http://lkml.indiana.edu/hypermail/linux/kernel/0508.2/0757.html描述的行爲:


你的測試用例是有問題的,因爲你不建立狀態甚至收到一個ACK,這樣巾> rcv_tstamp變量沒有辦法讓初始化。您收到的唯一的ACK是響應連接設置SYN的ACK,並且我們不會爲該ACK初始化tp-> rcv_stamp。

keepalive時間檢查絕對要求tp-> rcv_tstamp有一個有效的值,並且直到您在ESTABLISHED狀態下處理ACK時它不會。

如果您發送成功或在連接上成功接收至少一個字節,並且因此在ESTABLISHED狀態下處理至少一個ACK,我想您會發現Keepalive行爲正常。


這是一個隱晦的SO_KEEPALIVE行爲。

0

這是因爲tcp重傳由tcp堆棧沒有你的意識。 這裏有解決方案。

即使您已經爲您的應用程序套接字設置了keepalive選項,但您的應用程序一直在套接字上寫入的情況下,您無法及時檢測套接字的死連接狀態。 這是因爲內核tcp堆棧的tcp重傳。 tcp_retries1和tcp_retries2是用於配置tcp重新傳輸超時的內核參數。 很難預測精確的重傳超時時間,因爲它是通過RTT機制計算的。 你可以在rfc793中看到這個計算。 (3.7。數據通信)

https://www.rfc-editor.org/rfc/rfc793.txt

每個平臺具有用於TCP重發內核配置。

Linux : tcp_retries1, tcp_retries2 : (exist in /proc/sys/net/ipv4) 

http://linux.die.net/man/7/tcp

HPUX : tcp_ip_notify_interval, tcp_ip_abort_interval 

http://www.hpuxtips.es/?q=node/53

AIX : rto_low, rto_high, rto_length, rto_limit 

http://www-903.ibm.com/kr/event/download/200804_324_swma/socket.pdf

您應該tcp_retries2(默認爲15)如果你想及早發現死連接設置較低的值,但它的我已經沒有精確的時間了說過。 另外,目前您不能僅爲單個套接字設置這些值。這些是全局內核參數。 有一些嘗試爲單插槽應用tcp轉發套接字選項(http://patchwork.ozlabs.org/patch/55236/),但我認爲它不應用於內核主線。我無法在系統頭文件中找到這些選項定義。

僅供參考,您可以通過下面的'netstat --timers'來監控您的Keepalive套接字選項。 https://stackoverflow.com/questions/34914278

netstat -c --timer | grep "192.0.0.1:43245    192.0.68.1:49742" 

tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (1.92/0/0) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (0.71/0/0) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (9.46/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (8.30/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (7.14/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (5.98/0/1) 
tcp  0  0 192.0.0.1:43245    192.0.68.1:49742   ESTABLISHED keepalive (4.82/0/1) 

此外,當keepalive超時ocurrs,可滿足您不同的返回事件取決於您使用的平臺,所以你必須不返回事件決定只死連接狀態。 例如,當Keepalive超時發生時,HP返回POLLERR事件,並且AIX僅返回POLLIN事件。 您當時會在recv()調用中遇到ETIMEDOUT錯誤。

在最近的內核版本(自2.6.37開始)之後,可以使用TCP_USER_TIMEOUT選項將很好的工作。該選項可用於單個套接字。

相關問題