在 TCP 協議中,默認情況下,當我們調用 close() 函數關閉套接口時,TCP 走四次揮手進行斷開鏈路,但是要是若緩沖區還有數據未發送到對端時,系統將嘗試把這些數據發送給對端。四次揮手的過程導致我們在 TIME_WAIT 狀態下無法復用端口。有些情況下我們不需要 TIME_WAIT, 而是想快速斷開連接,從而避免 socket 的堆積。
這個時候我們可以使用 SO_LINGER 套接字選項
struct linger {
int l_onoff;
int l_linger;
}
- 若 l_onoff 為0, 表示關閉該選項。l_linger 值被忽略,也即是走TCP 的默認設置。
2)若 l_onoff 為非 0 且 l_linger 為 0,那么當 close 某個連接時 TCP 將終止該連接。也即是TCP將丟棄保留在套接字發送緩沖區中的任何數據,并發送RST報文給對端,不再走四次揮手,從而避免了 TCP 的 TIME_WAIT 狀態。但是依然存在以下可能性:在 2 MSL 秒內創建該連接的另一個化身,導致來自剛被終止的連接上的舊的重復分節被不正確的傳遞到新的化身上。
3)若 l_onoff 為非 0 值且 l_linger 也為非 0 值,那么當套接字關閉時內核將拖延一段時間關閉,也即是若在套接字的發送緩沖區中還有殘留數據,那么進程將投入睡眠,直到數據發送完且均被對端確認或者滯留時間到。若套接字被設置成非阻塞型,那么它將不等待 close 完成,即是滯留時間不為 0 也是如此。當使用 SO_LINGER 選項時,應用程序檢查 close 的返回值很重要,因為若在數據發送完并被確認前延滯時間到的話,close 將返回 EWOULDBLOCK 錯誤,且套接字發送緩沖區中的任何殘留數據都被丟棄。
通過下面實現進行驗證。
首先 server 端使用 nc 進行監聽一個TCP 指定端口。
客戶端使用如下代碼
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct sockaddr_in peer;
struct linger linger;
int ret;
int sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &peer.sin_addr);
peer.sin_port = htons(atoi(argv[2]));
memset(&linger, 0, sizeof(linger));
linger.l_onoff = 1;
linger.l_linger = 0;
ret = setsockopt(sock, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger));
if (ret) {
printf("Fail to set linger\\n");
exit(1);
}
ret = connect(sock, (const struct sockaddr *)&peer, sizeof(peer));
if (ret) {
printf("Fail to connect.\\n", strerror(errno));
exit(1);
}
printf("Connect successfully\\n");
close(sock);
printf("Done\\n");
return 0;
}
通過抓包分析來看,調用 close 后,客戶端直接發送了 RST 報文端開了連接。
19:22:13.101476 IP 17.15.220.199 > localhost.localdomain : Flags [S], seq 12771346 ..
19:22:13.101509 IP localhost.localdomain > 17.15.220.199 : Flags [S .], seq 1277234 ..
19:22:13.101732 IP 17.15.220.199 > localhost.localdomain : Flags [.], ack ...
19:22:13.101912 IP 17.15.220.199 > localhost.localdomain : Flags [R .] ...
在 tcp_close 中查看具體實現
/*
內核并并不關心有多少數據未被用戶進程讀取,內核關心的是有沒有數據未被讀取,
若有數據未被讀取而丟棄(data_was_unread>0),則給對方發送rst報文
若沒有數據未被用戶進程讀取,也即是全部數據都被用戶進程讀取了(data_was_unread==0),則相對對端發送fin報文
*/
if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONCLOSE);
/*發送rst報文前設置狀態為TCP_CLOSE,這時沒有TIME_WAIT狀態,沒有FIN_WAIT_1狀態,說明此時時不正常關閉的。
所以可得,在編寫程序時,在關閉連接前,一定要保證所有接收到的數據被讀取,否則連接會不正常關閉*/
tcp_set_state(sk, TCP_CLOSE);
//發送rst報文,之所以不是fin報文,是因為關閉時還有未讀的數據屬于異常情況,fin表示一切正常情況
tcp_send_active_reset(sk, GFP_KERNEL);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
/* Check zero linger _after_ checking for unread data. */
/*調用tcp_disconnect斷開、刪除并釋放已建立連接但未被accept的傳輸控制塊,同時
刪除并釋放已接收在接收隊列(包括失序隊列)上的段以及發送隊列上的段*/
sk->sk_prot->disconnect(sk, 0);// tcp_disconnect
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONDATA);
} else if (tcp_close_state(sk)) { //若未讀字節數為0,則調用tcp_close_state根據sk當前狀態來設置sk下一狀態,比如當前狀態為TCP_ESTABLISHED,則下一狀態為TCP_FIN_WAIT1,該方法的返回確定是否發送fin報文給對方
/*
從上面的代碼段可以看到,當有數據還未讀取時,說明是異常關閉,直接發送 RST 報文給對端。若接收緩沖區中數據都已經讀取完了,判斷 SOCK_LINGER 套接字選項,若 l_linger 為 0,則調用 tcp_disconnect 給對端發送 RST 報文,同時釋放接收和發送隊列上的數據。
審核編輯:劉清
-
RST
+關注
關注
0文章
31瀏覽量
7426 -
TCP協議
+關注
關注
1文章
101瀏覽量
12127
發布評論請先 登錄
相關推薦
評論