Ⅰ linux C語言 C/S程序,客戶端發送的數據和伺服器端接收到的數據不一樣,求解
估計你是用的tcp socket,導致「videlord」網友說的情況:對於tcp socket,send與recv不是對等的,recv時只要緩沖有數據就會收上來。簡單說就是你send 4次,比如分別為10 10 10 10位元組,對端可以一次recv到這40位元組數據,也可以recv 40次、每次1位元組。
解決辦法有兩種:
改用udp socket,send/recv自然對等了
繼續用tcp socket,自己進行數據分段:比如自行約定在數據前約定4個位元組用於描述數據長度,這樣發送時,send 4+33位元組,send 4+35位元組;接收時,先獲取描述長度的4位元組獲得長度,再按照長度接收數據(可能需要多次recv湊齊指定長度)。
Ⅱ Linux下c++ 服務端tcp通訊包延遲,是否有特殊的設定需要
客戶端向伺服器端發送請求,伺服器收到請求做相應的處理,將處理結果傳回客戶端。下面採用TCP協議實現伺服器和客戶端之間的連接。
1. 客戶端
約定雙方的傳輸協議(UDP或者TCP),根據傳輸協議創建socket;
伺服器的IP地址和埠號;
連接伺服器;
獲取伺服器傳遞回來的數據。
[cpp]
#include
#include types.h>
#include socket.h>
#include time.h>
#include
#include
#includein.h>
#includeinet.h>
#include errno.h>
#include
#include
#include
using namespace std;
const int MAXLINE=1024;
int main(int argc,char** argv)
{
int sockfd,n;
char recvline[MAXLINE+1];
struct sockaddr_in servaddr;
if(argc!=2)
{
cout<<"usage: a.out<IPaddress"<<endl;
exit(0);
}
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
cout<<"socket error"<<endl;
exit(0);
}
memset(&servaddr,0, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(8080);//將無符號短整型數值轉換為網路位元組序,即將數值的高位位元組存放到內存中的低位位元組0X1234變為0X3412
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0)//將ip地址在「點分十進制」和整數之間轉換
{
cout<<"inet_ptons error"<<endl;
exit(0);
}
if(connect(sockfd,(sockaddr*)&servaddr,sizeof(servaddr))<0)
{
cout<<"connect error"<<endl;
exit(0);
}
while((n=read(sockfd,recvline,MAXLINE))>0)
{
recvline[n]=0;
if(fputs(recvline,stdout)==EOF)
{
cout<<"fputs error"<<endl;
exit(0);
}
}
if(n<0)
{
cout<<"read error"<<endl;
exit(0);
}
exit(0);
}
#include
#include types.h>
#include socket.h>
#include time.h>
#include
#include
#includein.h>
#includeinet.h>
#include errno.h>
#include
#include
#include
using namespace std;
const int MAXLINE=1024;
int main(int argc,char** argv)
{
int sockfd,n;
char recvline[MAXLINE+1];
struct sockaddr_in servaddr;
if(argc!=2)
{
cout<<"usage: a.out<IPaddress"<<endl;
exit(0);
}
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
cout<<"socket error"<<endl;
exit(0);
}
memset(&servaddr,0, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(8080);//將無符號短整型數值轉換為網路位元組序,即將數值的高位位元組存放到內存中的低位位元組0X1234變為0X3412
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0)//將ip地址在「點分十進制」和整數之間轉換
{
cout<<"inet_ptons error"<<endl;
exit(0);
}
if(connect(sockfd,(sockaddr*)&servaddr,sizeof(servaddr))<0)
{
cout<<"connect error"<<endl;
exit(0);
}
while((n=read(sockfd,recvline,MAXLINE))>0)
{
recvline[n]=0;
if(fputs(recvline,stdout)==EOF)
{
cout<<"fputs error"<<endl;
exit(0);
}
}
if(n<0)
{
cout<<"read error"<<endl;
exit(0);
}
exit(0);
}
2. 伺服器
約定雙方的傳輸協議(UDP或者TCP),根據傳輸協議創建socket;
將地址和埠綁定到socket;
對埠進行偵聽,直到偵聽到有連接信息;
接收連接,然後將數據寫回連接中。
將地址和埠綁定到socket
[cpp]
#include
#include types.h>
#include socket.h>
#include time.h>
#include
#include
#includein.h>
#includeinet.h>
#include errno.h>
#include
#include
#include
#include
using namespace std;
const int MAXLINE=1024;
int main(int argc,char **argv)
{
int listenfd,connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
listenfd=socket(AF_INET,SOCK_STREAM,0);//建立socket
if(listenfd<0)
{
cout<<"socket error "<<strerror(errno)<<endl;
exit(0);
}
memset(&servaddr,0, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(8080);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
int bindfd=bind(listenfd,(sockaddr*)&servaddr,sizeof(servaddr));//將地址和埠綁定到socket
if(bindfd<0)
{
cout<<"bind error"<<bindfd<<endl;
exit(0);
}
listen(listenfd,MAXLINE);//監聽連接,一直阻塞,直到有連接出現
while(1)
{
connfd=accept(listenfd,(sockaddr*)NULL,NULL);//接收連接傳遞的數據
ticks=time(NULL);
snprintf(buff,sizeof(buff),"%.24s
",ctime(&ticks));
cout<<strlen(buff)<<endl;
write(connfd,buff,strlen(buff));//將傳遞的數據寫回到socket中
close(connfd);
}
return 0;
}
#include
#include types.h>
#include socket.h>
#include time.h>
#include
#include
#includein.h>
#includeinet.h>
#include errno.h>
#include
#include
#include
#include
using namespace std;
const int MAXLINE=1024;
int main(int argc,char **argv)
{
int listenfd,connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
listenfd=socket(AF_INET,SOCK_STREAM,0);//建立socket
if(listenfd<0)
{
cout<<"socket error "<<strerror(errno)<<endl;
exit(0);
}
memset(&servaddr,0, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(8080);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
int bindfd=bind(listenfd,(sockaddr*)&servaddr,sizeof(servaddr));//將地址和埠綁定到socket
if(bindfd<0)
{
cout<<"bind error"<<bindfd<<endl;
exit(0);
}
listen(listenfd,MAXLINE);//監聽連接,一直阻塞,直到有連接出現
while(1)
{
connfd=accept(listenfd,(sockaddr*)NULL,NULL);//接收連接傳遞的數據
ticks=time(NULL);
snprintf(buff,sizeof(buff),"%.24s
",ctime(&ticks));
cout<<strlen(buff)<<endl;
write(connfd,buff,strlen(buff));//將傳遞的數據寫回到socket中
close(connfd);
}
return 0;
}
Ⅲ linux 下socket的recv函數返回值問題
說清楚一點,是UDP還是TCP
不管怎麼說,在recv之前調用一下select(),檢查緩沖到底有沒有內容,如果有,再執行recv就不會有任何問題。而且 select的好處是,如果沒有接到別的東西,你可以sleep()一下,不佔用CPU
用下面的rcv代替你的recv函數吧,我在嵌入式系統開發時自己寫的一個標准常式,很可靠:
參數解釋:
sck - socket
buf - 接收緩沖區
size-緩沖區大小
time_out-等待時間(按秒計)如果超時則返回
返回值:收到位元組數,0表示超時等錯誤
int rcv(int sck, void * buf, int size, int time_out)
{
if (sck < 1 || !buf || size < 1) return 0;
timeval tv = { 0, 0}; timeval * ptv = 0;
if (time_out > 0) { tv.tv_sec = time_out; ptv = &tv; }
memset(buf, 0, size);
int r = 0; char * b = (char*) buf; int sz = size;
fd_set rd, er; int total = 0; time_t t0 = time(0); time_t t1 = 0;
do {
FD_ZERO(&rd); FD_SET(sck, &rd);
FD_ZERO(&er); FD_SET(sck, &er);
r = select(sck + 1, &rd, 0, &er, ptv);
if (r == -1) { nperror("select()"); return -1; }
if (FD_ISSET(sck, &er)) {
nperror("socket(shutdown)"); return -1;
}//end if
if (FD_ISSET(sck, &rd)) {
r = recv(sck, b, sz, 0);
if (r == -1) { nperror("recv()"); return -1; }
total += r; sz -= r; b+= r;
}//end if
if (time_out > 0)
t1 = time(0) - t0;
else
t1 = time_out - 1;
//end if
}while(sz && t1 < time_out);
return total;
}//end if
Ⅳ linux中的socket編程的問題,recv函數收到的全是0
send後,操作系統會決定是不是馬山發包還是繼續等待看看還有別的send,然後把他們一起打成一個包來發送。所以你recv是不知道要接收幾個包的。
Ⅳ 關於 Linux 網路,你必須知道這些
我們一起學習了文件系統和磁碟 I/O 的工作原理,以及相應的性能分析和優化方法。接下來,我們將進入下一個重要模塊—— Linux 的網路子系統。
由於網路處理的流程最復雜,跟我們前面講到的進程調度、中斷處理、內存管理以及 I/O 等都密不可分,所以,我把網路模塊作為最後一個資源模塊來講解。
同 CPU、內存以及 I/O 一樣,網路也是 Linux 系統最核心的功能。網路是一種把不同計算機或網路設備連接到一起的技術,它本質上是一種進程間通信方式,特別是跨系統的進程間通信,必須要通過網路才能進行。隨著高並發、分布式、雲計算、微服務等技術的普及,網路的性能也變得越來越重要。
說到網路,我想你肯定經常提起七層負載均衡、四層負載均衡,或者三層設備、二層設備等等。那麼,這里說的二層、三層、四層、七層又都是什麼意思呢?
實際上,這些層都來自國際標准化組織制定的開放式系統互聯通信參考模型(Open System Interconnection Reference Model),簡稱為 OSI 網路模型。
但是 OSI 模型還是太復雜了,也沒能提供一個可實現的方法。所以,在 Linux 中,我們實際上使用的是另一個更實用的四層模型,即 TCP/IP 網路模型。
TCP/IP 模型,把網路互聯的框架分為應用層、傳輸層、網路層、網路介面層等四層,其中,
為了幫你更形象理解 TCP/IP 與 OSI 模型的關系,我畫了一張圖,如下所示:
當然了,雖說 Linux 實際按照 TCP/IP 模型,實現了網路協議棧,但在平時的學習交流中,我們習慣上還是用 OSI 七層模型來描述。比如,說到七層和四層負載均衡,對應的分別是 OSI 模型中的應用層和傳輸層(而它們對應到 TCP/IP 模型中,實際上是四層和三層)。
OSI引入了服務、介面、協議、分層的概念,TCP/IP借鑒了OSI的這些概念建立TCP/IP模型。
OSI先有模型,後有協議,先有標准,後進行實踐;而TCP/IP則相反,先有協議和應用再提出了模型,且是參照的OSI模型。
OSI是一種理論下的模型,而TCP/IP已被廣泛使用,成為網路互聯事實上的標准。
有了 TCP/IP 模型後,在進行網路傳輸時,數據包就會按照協議棧,對上一層發來的數據進行逐層處理;然後封裝上該層的協議頭,再發送給下一層。
當然,網路包在每一層的處理邏輯,都取決於各層採用的網路協議。比如在應用層,一個提供 REST API 的應用,可以使用 HTTP 協議,把它需要傳輸的 JSON 數據封裝到 HTTP 協議中,然後向下傳遞給 TCP 層。
而封裝做的事情就很簡單了,只是在原來的負載前後,增加固定格式的元數據,原始的負載數據並不會被修改。
比如,以通過 TCP 協議通信的網路包為例,通過下面這張圖,我們可以看到,應用程序數據在每個層的封裝格式。
這些新增的頭部和尾部,增加了網路包的大小,但我們都知道,物理鏈路中並不能傳輸任意大小的數據包。網路介面配置的最大傳輸單元(MTU),就規定了最大的 IP 包大小。在我們最常用的乙太網中,MTU 默認值是 1500(這也是 Linux 的默認值)。
一旦網路包超過 MTU 的大小,就會在網路層分片,以保證分片後的 IP 包不大於 MTU 值。顯然,MTU 越大,需要的分包也就越少,自然,網路吞吐能力就越好。
理解了 TCP/IP 網路模型和網路包的封裝原理後,你很容易能想到,Linux 內核中的網路棧,其實也類似於 TCP/IP 的四層結構。如下圖所示,就是 Linux 通用 IP 網路棧的示意圖:
我們從上到下來看這個網路棧,你可以發現,
這里我簡單說一下網卡。網卡是發送和接收網路包的基本設備。在系統啟動過程中,網卡通過內核中的網卡驅動程序注冊到系統中。而在網路收發過程中,內核通過中斷跟網卡進行交互。
再結合前面提到的 Linux 網路棧,可以看出,網路包的處理非常復雜。所以,網卡硬中斷只處理最核心的網卡數據讀取或發送,而協議棧中的大部分邏輯,都會放到軟中斷中處理。
我們先來看網路包的接收流程。
當一個網路幀到達網卡後,網卡會通過 DMA 方式,把這個網路包放到收包隊列中;然後通過硬中斷,告訴中斷處理程序已經收到了網路包。
接著,網卡中斷處理程序會為網路幀分配內核數據結構(sk_buff),並將其拷貝到 sk_buff 緩沖區中;然後再通過軟中斷,通知內核收到了新的網路幀。
接下來,內核協議棧從緩沖區中取出網路幀,並通過網路協議棧,從下到上逐層處理這個網路幀。比如,
最後,應用程序就可以使用 Socket 介面,讀取到新接收到的數據了。
為了更清晰表示這個流程,我畫了一張圖,這張圖的左半部分表示接收流程,而圖中的粉色箭頭則表示網路包的處理路徑。
了解網路包的接收流程後,就很容易理解網路包的發送流程。網路包的發送流程就是上圖的右半部分,很容易發現,網路包的發送方向,正好跟接收方向相反。
首先,應用程序調用 Socket API(比如 sendmsg)發送網路包。
由於這是一個系統調用,所以會陷入到內核態的套接字層中。套接字層會把數據包放到 Socket 發送緩沖區中。
接下來,網路協議棧從 Socket 發送緩沖區中,取出數據包;再按照 TCP/IP 棧,從上到下逐層處理。比如,傳輸層和網路層,分別為其增加 TCP 頭和 IP 頭,執行路由查找確認下一跳的 IP,並按照 MTU 大小進行分片。
分片後的網路包,再送到網路介面層,進行物理地址定址,以找到下一跳的 MAC 地址。然後添加幀頭和幀尾,放到發包隊列中。這一切完成後,會有軟中斷通知驅動程序:發包隊列中有新的網路幀需要發送。
最後,驅動程序通過 DMA ,從發包隊列中讀出網路幀,並通過物理網卡把它發送出去。
多台伺服器通過網卡、交換機、路由器等網路設備連接到一起,構成了相互連接的網路。由於網路設備的異構性和網路協議的復雜性,國際標准化組織定義了一個七層的 OSI 網路模型,但是這個模型過於復雜,實際工作中的事實標准,是更為實用的 TCP/IP 模型。
TCP/IP 模型,把網路互聯的框架,分為應用層、傳輸層、網路層、網路介面層等四層,這也是 Linux 網路棧最核心的構成部分。
我結合網路上查閱的資料和文章中的內容,總結了下網卡收發報文的過程,不知道是否正確:
當發送數據包時,與上述相反。鏈路層將數據包封裝完畢後,放入網卡的DMA緩沖區,並調用系統硬中斷,通知網卡從緩沖區讀取並發送數據。
了解 Linux 網路的基本原理和收發流程後,你肯定迫不及待想知道,如何去觀察網路的性能情況。具體而言,哪些指標可以用來衡量 Linux 的網路性能呢?
實際上,我們通常用帶寬、吞吐量、延時、PPS(Packet Per Second)等指標衡量網路的性能。
除了這些指標,網路的可用性(網路能否正常通信)、並發連接數(TCP 連接數量)、丟包率(丟包百分比)、重傳率(重新傳輸的網路包比例)等也是常用的性能指標。
分析網路問題的第一步,通常是查看網路介面的配置和狀態。你可以使用 ifconfig 或者 ip 命令,來查看網路的配置。我個人更推薦使用 ip 工具,因為它提供了更豐富的功能和更易用的介面。
以網路介面 eth0 為例,你可以運行下面的兩個命令,查看它的配置和狀態:
你可以看到,ifconfig 和 ip 命令輸出的指標基本相同,只是顯示格式略微不同。比如,它們都包括了網路介面的狀態標志、MTU 大小、IP、子網、MAC 地址以及網路包收發的統計信息。
第一,網路介面的狀態標志。ifconfig 輸出中的 RUNNING ,或 ip 輸出中的 LOWER_UP ,都表示物理網路是連通的,即網卡已經連接到了交換機或者路由器中。如果你看不到它們,通常表示網線被拔掉了。
第二,MTU 的大小。MTU 默認大小是 1500,根據網路架構的不同(比如是否使用了 VXLAN 等疊加網路),你可能需要調大或者調小 MTU 的數值。
第三,網路介面的 IP 地址、子網以及 MAC 地址。這些都是保障網路功能正常工作所必需的,你需要確保配置正確。
第四,網路收發的位元組數、包數、錯誤數以及丟包情況,特別是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指標不為 0 時,通常表示出現了網路 I/O 問題。其中:
ifconfig 和 ip 只顯示了網路介面收發數據包的統計信息,但在實際的性能問題中,網路協議棧中的統計信息,我們也必須關注。你可以用 netstat 或者 ss ,來查看套接字、網路棧、網路介面以及路由表的信息。
我個人更推薦,使用 ss 來查詢網路的連接信息,因為它比 netstat 提供了更好的性能(速度更快)。
比如,你可以執行下面的命令,查詢套接字信息:
netstat 和 ss 的輸出也是類似的,都展示了套接字的狀態、接收隊列、發送隊列、本地地址、遠端地址、進程 PID 和進程名稱等。
其中,接收隊列(Recv-Q)和發送隊列(Send-Q)需要你特別關注,它們通常應該是 0。當你發現它們不是 0 時,說明有網路包的堆積發生。當然還要注意,在不同套接字狀態下,它們的含義不同。
當套接字處於連接狀態(Established)時,
當套接字處於監聽狀態(Listening)時,
所謂全連接,是指伺服器收到了客戶端的 ACK,完成了 TCP 三次握手,然後就會把這個連接挪到全連接隊列中。這些全連接中的套接字,還需要被 accept() 系統調用取走,伺服器才可以開始真正處理客戶端的請求。
與全連接隊列相對應的,還有一個半連接隊列。所謂半連接是指還沒有完成 TCP 三次握手的連接,連接只進行了一半。伺服器收到了客戶端的 SYN 包後,就會把這個連接放到半連接隊列中,然後再向客戶端發送 SYN+ACK 包。
類似的,使用 netstat 或 ss ,也可以查看協議棧的信息:
這些協議棧的統計信息都很直觀。ss 只顯示已經連接、關閉、孤兒套接字等簡要統計,而 netstat 則提供的是更詳細的網路協議棧信息。
比如,上面 netstat 的輸出示例,就展示了 TCP 協議的主動連接、被動連接、失敗重試、發送和接收的分段數量等各種信息。
接下來,我們再來看看,如何查看系統當前的網路吞吐量和 PPS。在這里,我推薦使用我們的老朋友 sar,在前面的 CPU、內存和 I/O 模塊中,我們已經多次用到它。
給 sar 增加 -n 參數就可以查看網路的統計信息,比如網路介面(DEV)、網路介面錯誤(EDEV)、TCP、UDP、ICMP 等等。執行下面的命令,你就可以得到網路介面統計信息:
這兒輸出的指標比較多,我來簡單解釋下它們的含義。
其中,Bandwidth 可以用 ethtool 來查詢,它的單位通常是 Gb/s 或者 Mb/s,不過注意這里小寫字母 b ,表示比特而不是位元組。我們通常提到的千兆網卡、萬兆網卡等,單位也都是比特。如下你可以看到,我的 eth0 網卡就是一個千兆網卡:
其中,Bandwidth 可以用 ethtool 來查詢,它的單位通常是 Gb/s 或者 Mb/s,不過注意這里小寫字母 b ,表示比特而不是位元組。我們通常提到的千兆網卡、萬兆網卡等,單位也都是比特。如下你可以看到,我的 eth0 網卡就是一個千兆網卡:
我們通常使用帶寬、吞吐量、延時等指標,來衡量網路的性能;相應的,你可以用 ifconfig、netstat、ss、sar、ping 等工具,來查看這些網路的性能指標。
小狗同學問到: 老師,您好 ss —lntp 這個 當session處於listening中 rec-q 確定是 syn的backlog嗎?
A: Recv-Q為全連接隊列當前使用了多少。 中文資料里這個問題講得最明白的文章: https://mp.weixin.qq.com/s/yH3PzGEFopbpA-jw4MythQ
看了源碼發現,這個地方講的有問題.關於ss輸出中listen狀態套接字的Recv-Q表示全連接隊列當前使用了多少,也就是全連接隊列的當前長度,而Send-Q表示全連接隊列的最大長度
Ⅵ linux系統中,tcp灌包連接失敗是什麼原因
沒有將回應包發送到客戶端。linux系統中,tcp灌包連接失敗的原因是沒有將回應包發送到客戶端的35425埠,因此客戶端認為建立TCP連接失敗,表現出現的現象就是連接斷線或網路斷開等。
Ⅶ 暢談linux下TCP(上)
tcp 協議 是互聯網中最常用的協議 , 開發人員基本上天天和它打交道,對它進行深入了解。 可以幫助我們排查定位bug和進行程序優化。下面我將就TCP幾個點做深入的探討
客戶端:收到 ack 後 分配連接資源。 發送數據
伺服器 : 收到 syn 後立即 分配連接資源
客戶端:收到ACK, 立即分配資源
伺服器:收到ACK, 立即分配資源
既然三次握手也不是100%可靠, 那四次,五次,六次。。。呢? 其實都一樣,不管多少次都有丟包問題。
client 只發送一個 SYN, server 分配一個tcb, 放入syn隊列中。 這時候連接叫 半連接 狀態;如果server 收不到 client 的ACK, 會不停重試 發送 ACK-SYN 給client 。重試間隔 為 2 的 N 次方 疊加(2^0 , 2^1, 2^2 ....);直至超時才釋放syn隊列中的這個 TCB;
在半連接狀態下, 一方面會佔用隊列配額資源,另一方面佔用內存資源。我們應該讓半連接狀態存在時間盡可能的小
當client 向一個未打開的埠發起連接請求時,會收到一個RST回復包
當listen 的 backlog 和 somaxconn 都設置了得時候, 取兩者min值
Recv-Q 是accept 隊列當前個數, Send-Q 設置最大值
這種SYN洪水攻擊是一種常見攻擊方式,就是利用半連接隊列特性,占滿syn 隊列的 資源,導致 client無法連接上。
解決方案:
為什麼不像握手那樣合並成三次揮手? 因為和剛開始連接情況,連接是大家都從0開始, 關閉時有歷史包袱的。server(被動關閉方) 收到 client(主動關閉方) 的關閉請求FIN包。 這時候可能還有未發送完的數據,不能丟棄。 所以需要分開。事實可能是這樣
當然,在沒有待發數據,並且允許 Delay ACK 情況下, FIN-ACK合並還是非常常見的事情,這是三次揮手是可以的。
同上
CLOSE_WAIT 是被動關閉方才有的狀態 。
被動關閉方 [收到 FIN 包 發送 ACK 應答] 到 [發送FIN, 收到ACK ] 期間的狀態為 CLOSE_WAIT, 這個狀態仍然能發送數據。 我們叫做 半關閉 , 下面用個例子來分析:
這個是我實際生產環境碰到的一個問題,長連接會話場景,server端收到client的rpc call 請求1,處理發現請求包有問題,就強制關閉結束這次會話, 但是 因為client 發送 第二次請求之前,並沒有去調用recv,所以並不知道 這個連接被server關閉, 繼續發送 請求2 , 此時是半連接,能夠成功發送到對端機器,但是recv結果後,遇到連接已經關閉錯誤。
如果 client 和 server 恰好同時發起關閉連接。這種情況下,兩邊都是主動連接,都會進入 TIME_WAIT狀態
1、 被動關閉方在LAST_ACK狀態(已經發送FIN),等待主動關閉方的ACK應答,但是 ACK丟掉, 主動方並不知道,以為成功關閉。因為沒有TIME_WAIT等待時間,可以立即創建新的連接, 新的連接發送SYN到前面那個未關閉的被動方,被動方認為是收到錯誤指令,會發送RST。導致創建連接失敗。
2、 主動關閉方斷開連接,如果沒有TIME_WAIT等待時間,可以馬上建立一個新的連接,但是前一個已經斷開連接的,延遲到達的數據包。 被新建的連接接收,如果剛好seq 和 ack欄位 都正確, seq在滑動窗口范圍內(只能說機率非常小,但是還是有可能會發生),會被當成正確數據包接收,導致數據串包。 如果不在window范圍內,則沒有影響( 發送一個確認報文(ack 欄位為期望ack的序列號,seq為當前發送序列號),狀態變保持原樣)
TIME_WAIT 問題比較比較常見,特別是CGI機器,並發量高,大量連接後段服務的tcp短連接。因此也衍生出了多種手段解決。雖然每種方法解決不是那麼完美,但是帶來的好處一般多於壞處。還是在日常工作中會使用。
1、改短TIME_WAIT 等待時間
這個是第一個想到的解決辦法,既然等待時間太長,就改成時間短,快速回收埠。但是實際情況往往不樂觀,對於並發的機器,你改多短才能保證回收速度呢,有時候幾秒鍾就幾萬個連接。太短的話,就會有前面兩種問題小概率發生。
2、禁止Socket lingering
這種情況下關閉連接,會直接拋棄緩沖區中待發送的數據,會發送一個RST給對端,相當於直接拋棄TIME_WAIT, 進入CLOSE狀態。同樣因為取消了 TIME_WAIT 狀態,會有前面兩種問題小概率發生。
3、tcp_tw_reuse
net.ipv4.tcp_tw_reuse選項是 從 TIME_WAIT 狀態的隊列中,選取條件:1、remote 的 ip 和埠相同, 2、選取一個時間戳小於當前時間戳; 用來解決埠不足的尷尬。
現在埠可以復用了,看看如何面對前面TIME_WAIT 那兩種問題。 我們仔細回顧用一下前面兩種問題。 都是在新建連接中收到老連接的包導致的問題 , 那麼如果我能在新連接中識別出此包為非法包,是不是就可以丟掉這些無用包,解決問題呢。
需要實現這些功能,需要擴展一下tcp 包頭。 增加 時間戳欄位。 發送者 在每次發送的時候。 在tcp包頭裡面帶上發送時候的時間戳。 當接收者接收的時候,在ACK應答中除了TCP包頭中帶自己此時發送的時間戳,並且把收到的時間戳附加在後面。也就是說ACK包中有兩個時間戳欄位。結構如下:
那我們接下來一個個分析tcp_tw_reuse是如何解決TIME_WAIT的兩個問題的
4、tcp_tw_recycle
tcp_tw_recycle 也是藉助 timestamp機制。顧名思義, tcp_tw_reuse 是復用 埠,並不會減少 TIME-WAIT 數量。你去查詢機器上TIME-WAIT 數量,還是 幾千幾萬個,這點對有強迫症的同學感覺很不舒服。tcp_tw_recycle 是 提前 回收 TIME-WAIT資源。會減少 機器上 TIME-WAIT 數量。
tcp_tw_recycle 工作原理是。