Linux上TCP丟失小包不可見的解決
作者:千裡孤行(http://blog.csdn.net/yanghehong)
有網友在做一個Linux上的用戶端碰到這樣的問題:
我們在Linux下開發一個網路用戶端程式(伺服器是不可修改的),不斷向伺服器不定期發送一些很小的包(一般只有幾十位元組),現在出現這種情況:大部分包正常發送,但是當無線Modem斷開的時候,如果這個時候剛好有一個小包,就可能丟失,但在程式中卻顯示已經發送成功,導致丟包。
我們分析原因是這樣:
1:主程式建立Socket,TCP/IP方式,並採用Stream方式
2:主程式調用Write,寫入小包到系統的Socket緩衝區,並返回成功寫入,由於位元組數很小,所以一般都立即返回寫入成功!
3:Linux TCP/IP協議棧把Socket緩衝區資料發送到伺服器
如果第二步完成,剛好在第三步出現無線Modem斷開的情況,就會導致主程式以為已經發送成功了,但伺服器收不到的情況。
我們在網路上搜尋了大量的資料,但是沒有找到最終的解決方案,一般都是要求修改協議,加上對小包的ACK處理,但我們這邊無法控制伺服器。
我們也試著控制KeepAlive和NoDelay,但還是沒效果:
//對sock_cli設定KEEPALIVE和NODELAY
len = sizeof(unsigned int);
setsockopt(sock_cli, SOL_SOCKET, SO_KEEPALIVE, &optval, len);//使用KEEPALIVE
setsockopt(sock_cli, IPPROTO_TCP, TCP_NODELAY, &optval, len);//禁用NAGLE演算法
請問有什麼方式可以解決這種情況?
如果是Windows的,可以把核心的發送buffer設為0,也就是socket的SO_SNDBUF選項。那麼直到伺服器TCP收到資料並ack了,用戶端的寫入才返回成功。
不過這種設SO_SNDBUF的方法在Linux上是行不通的。Linux不讓把發送buffer設為0。
Linux核心中的代碼是這樣的:
Socket.c
int sock_setsockopt(struct socket *sock, int level, int optname,<br /> char __user *optval, int optlen)<br />{<br /> case SO_SNDBUF:<br /> /* Don't error on this BSD doesn't and if you think<br /> about it this is right. Otherwise apps have to<br /> play 'guess the biggest size' games. RCVBUF/SNDBUF<br /> are treated in BSD as hints */</p><p> if (val > sysctl_wmem_max)<br /> val = sysctl_wmem_max;<br />set_sndbuf:<br /> sk->sk_userlocks |= SOCK_SNDBUF_LOCK;<br /> if ((val * 2) < SOCK_MIN_SNDBUF)<br /> sk->sk_sndbuf = SOCK_MIN_SNDBUF;<br /> else<br /> sk->sk_sndbuf = val * 2;</p><p>
可以看到,socket的發送緩衝區sk_sndbuf是不讓你設的太大或太小的。你給的值太小(包括0),sk_sndbuf會被設為預設值SOCK_MIN_SNDBUF,也就是2048.
以上多種方法(包括網友已經嘗試的多種方法),都在努力把資料即刻發出去,想在網路斷開時發送完畢,結果都失敗。很多同類型方法都行不通的時候,可以試試換個思路。
該問題換一種說法是,當無線modem斷開時,有資料發送沒成功,應用程式卻誤判為成功。
既然發送失敗(有殘留沒發送出去)已經不可避免,那麼就承認這個失敗。在這種情況下,用戶端要多做一步,找出這些殘留有多少。
使用Linux的這個API
ioctl(tcp_socket, SIOCOUTQ, &value);
可以得到TCP socket發送隊列裡頭還沒有發送出去的資料有多少。
如果不為0,那麼證明最後一次發送是沒有成功的,儘管寫入buffer成功。這時用戶端程式就顯示失敗就可以了。