「前言」文章內(nèi)容大致是傳輸層協(xié)議,TCP協(xié)議講解的第二篇,續(xù)上篇TCP。
「歸屬專欄」網(wǎng)絡(luò)編程
「主頁(yè)鏈接」個(gè)人主頁(yè)
「筆者」楓葉先生(fy)
二、TCP協(xié)議
2.9 TCP連接管理機(jī)制
首先明確,TCP是面向連接的,TCP通信之前需要先建立連接,就是因?yàn)?strong>TCP的各種可靠性保證都是基于連接的,要保證傳輸數(shù)據(jù)的可靠性的前提就是先建立好連接。
TCP連接不直接保證可靠性,但是會(huì)間接保證可靠性
TCP進(jìn)行連接會(huì)進(jìn)行三次握手,斷開連接會(huì)進(jìn)行四次揮手。
TCP協(xié)議的客戶端/服務(wù)器程序的一般流程:
2.9.1 三次握手
雙方在進(jìn)行TCP通信之前需要先建立連接,建立連接的這個(gè)過(guò)程我們稱之為三次握手。
三次握手的流程如下:
- 第一次握手:客戶端向服務(wù)器發(fā)送一個(gè)SYN(同步)報(bào)文,請(qǐng)求與服務(wù)器建立連接。
- 第二次握手:服務(wù)器收到客戶端的SYN報(bào)文后,向客戶端發(fā)送一個(gè)SYN/ACK(同步/確認(rèn))報(bào)文,表示同意建立連接。
- 第三次握手:客戶端收到服務(wù)器的SYN/ACK報(bào)文后,向服務(wù)器發(fā)送一個(gè)ACK(確認(rèn))報(bào)文,表示連接建立成功。
通過(guò)三次握手,客戶端和服務(wù)器都確認(rèn)了對(duì)方的請(qǐng)求,并建立了可靠的連接。
注意:三次握手是連接通信的策略,即三次握手也可能會(huì)出現(xiàn)失敗的情況
為什么是三次握手??一次握手、兩次握手、四次握手行不行??
首先明確,三次握手是策略,不一定百分之百成功,也可能出現(xiàn)失敗。
比如,通信雙方在進(jìn)行三次握手時(shí),其中前兩次握手能夠保證被對(duì)方收到,因?yàn)榍皟纱挝帐侄加袑?duì)應(yīng)的應(yīng)答,但第三次握手是沒(méi)有對(duì)應(yīng)的應(yīng)答報(bào)文的,如果第三次握手時(shí)客戶端發(fā)送的ACK報(bào)文丟失了,那么連接建立就會(huì)失敗。
但是,我們不怕失敗丟包,因?yàn)門CP有配套的解決方案:
- 前面兩次握手成功后,在客戶端看來(lái),連接已經(jīng)建立好了,但是在服務(wù)端看來(lái),連接還沒(méi)有建立好。
- 如果第三層握手ACK發(fā)生丟包了,一段時(shí)間后服務(wù)端還是沒(méi)有收到ACK報(bào)文,此時(shí)服務(wù)端就會(huì),重新發(fā)起SYN/ACK(同步/確認(rèn))報(bào)文
- 又或者客戶端認(rèn)為連接建立好之后,就直接發(fā)數(shù)據(jù)報(bào)文了,此時(shí)服務(wù)端還沒(méi)有收到ACK確認(rèn),即服務(wù)端看來(lái)連接還沒(méi)有建立好,此時(shí)服務(wù)端就會(huì)收到該數(shù)據(jù)報(bào)文,服務(wù)端說(shuō):你都沒(méi)有跟我建立連接,發(fā)什么數(shù)據(jù)報(bào)文,重連。此時(shí)服務(wù)端就會(huì)把
RST
標(biāo)志位設(shè)置為1,發(fā)給客戶端,讓客戶端與服務(wù)端進(jìn)行重連
注意:第一次和第二次握手不攜帶數(shù)據(jù),第三次握手可能會(huì)攜帶數(shù)據(jù)
一次握手行不行?
絕對(duì)不行的。
- 首先明確,連接是需要被管理起來(lái)的,被OS管理起來(lái),如果管理連接??先描述,再組織。OS維護(hù)一個(gè)連接是有成本的。
- 即如果連接過(guò)多,OS管理不過(guò)來(lái),即代表服務(wù)器要寄了
一次握手的話,只要客戶端發(fā)起連接,就可以直接建立連接了(服務(wù)端認(rèn)為連接已經(jīng)建立好了),這樣就會(huì)導(dǎo)致單主機(jī)下SYN洪水攻擊
一次握手會(huì)發(fā)生SYN洪水攻擊,就是有人搞事,通過(guò)大量偽造的SYN報(bào)文向目標(biāo)服務(wù)器發(fā)送連接請(qǐng)求,從而消耗服務(wù)器資源,當(dāng)服務(wù)器的半連接隊(duì)列被耗盡后,合法用戶的連接請(qǐng)求無(wú)法被處理,導(dǎo)致服務(wù)不可用。
還有一點(diǎn)就是無(wú)法驗(yàn)證全雙工,即無(wú)法保證全雙工通信通道是流暢的,因?yàn)門CP是全雙工的
二次握手同上
為什么三次握手可以?
因?yàn)槿挝帐质球?yàn)證全雙工通信信道流暢的最小次數(shù)
- TCP是全雙工通信的,因此連接建立的核心要?jiǎng)?wù)實(shí)際是,驗(yàn)證雙方的通信信道是否是連通的。
- 而三次握手恰好是驗(yàn)證雙方通信信道的最小次數(shù),通過(guò)三次握手后雙方就都能知道自己和對(duì)方是否都能夠正常發(fā)送和接收數(shù)據(jù)
- 還有一點(diǎn)就是有效規(guī)避單主機(jī)下的SYN洪水攻擊
注意:TCP的工作是建立通信信道,服務(wù)器受到攻擊本身就不是TCP要解決的。但是如果三次握手有明顯的漏洞,讓客戶端利用了,這就是你TCP的問(wèn)題了
四次握手行不行?五次、六次…呢?
- 三次握手已經(jīng)是最小成本驗(yàn)證了全雙工,再多余就是浪費(fèi)時(shí)間
三次握手也可以叫四次握手,原因如下:
- 第二次握手時(shí),服務(wù)器收到客戶端的SYN報(bào)文后,向客戶端發(fā)送一個(gè)SYN/ACK(同步/確認(rèn))報(bào)文,這個(gè)SYN/ACK報(bào)文也可以分兩次發(fā)送給客戶端
- 但是這種情況幾乎不存在。
三次握手時(shí)的狀態(tài)變化
服務(wù)端:
- [
CLOSED -> LISTEN
] 服務(wù)器端調(diào)用listen后進(jìn)入LISTEN
狀態(tài),等待客戶端連接 - [
LISTEN -> SYN_RCVD
] 一旦監(jiān)聽到連接請(qǐng)求(同步報(bào)文段),就將該連接放入內(nèi)核等待隊(duì)列中,并向客戶端發(fā)送SYN確認(rèn)報(bào)文 - [
SYN_RCVD -> ESTABLISHED
] 服務(wù)端一旦收到客戶端的確認(rèn)報(bào)文,就進(jìn)入ESTABLISHED
狀態(tài),可以進(jìn)行讀寫數(shù)據(jù)了
客戶端:
- [
CLOSED -> SYN_SENT
] 客戶端調(diào)用connect,發(fā)送同步報(bào)文段 - [
SYN_SENT -> ESTABLISHED
] connect調(diào)用成功,則進(jìn)入ESTABLISHED
狀態(tài),開始讀寫數(shù)據(jù)
2.9.2 四次揮手
TCP通信結(jié)束之后就需要斷開連接,斷開連接的這個(gè)過(guò)程我們稱之為四次揮手。
四次揮手的過(guò)程如下:
- 第一次揮手:客戶端向服務(wù)器發(fā)送一個(gè)FIN報(bào)文,表示客戶端不再發(fā)送數(shù)據(jù)。
- 第二次揮手:服務(wù)器接收到客戶端的FIN報(bào)文后,向客戶端發(fā)送一個(gè)ACK報(bào)文,表示已經(jīng)收到客戶端的斷開請(qǐng)求。
- 第三次揮手:服務(wù)器向客戶端發(fā)送一個(gè)FIN報(bào)文,表示服務(wù)器也不再發(fā)送數(shù)據(jù)。
- 第四次揮手:客戶端接收到服務(wù)器的FIN報(bào)文后,向服務(wù)器發(fā)送一個(gè)ACK報(bào)文,表示已經(jīng)收到服務(wù)器的斷開請(qǐng)求。
這樣,雙方都確認(rèn)對(duì)方已經(jīng)斷開連接,完成四次揮手后,TCP連接就徹底關(guān)閉。
為什么要四次揮手?
- TCP是全雙工的通信協(xié)議,建立連接時(shí)需要雙方都確認(rèn)建立連接,而斷開連接時(shí)需要雙方都確認(rèn)斷開連接。因此,四次揮手的過(guò)程是必要的。
- 即斷開連接需要征得雙方的同意
- 每?jī)纱螕]手對(duì)應(yīng)就是關(guān)閉一個(gè)方向的通信信道,因此斷開連接時(shí)需要進(jìn)行四次揮手
四次揮手也可能變成三次揮手,原因如下:
- 第二次揮手:服務(wù)器接收到客戶端的FIN報(bào)文后,向客戶端發(fā)送一個(gè)ACK報(bào)文,表示已經(jīng)收到客戶端的斷開請(qǐng)求。
- 第三次揮手:服務(wù)器向客戶端發(fā)送一個(gè)FIN報(bào)文,表示服務(wù)器也不再發(fā)送數(shù)據(jù)
- 這兩個(gè)報(bào)文可能會(huì)合在一起發(fā),即ACK+FIN
- 因?yàn)镕IN和ACK都是不同的標(biāo)志位,不會(huì)影響雙方
四次揮手時(shí)的狀態(tài)變化
客戶端狀態(tài)轉(zhuǎn)化:
- [
FIN_WAIT_1 -> FIN_WAIT_2
] 客戶端收到服務(wù)器對(duì)結(jié)束報(bào)文段的確認(rèn),則進(jìn)入FIN_WAIT_2
,開始等待服務(wù)器的結(jié)束報(bào)文段 - [
FIN_WAIT_2 -> TIME_WAIT
] 客戶端收到服務(wù)器發(fā)來(lái)的結(jié)束報(bào)文段,進(jìn)入TIME_WAIT
, 并發(fā)出LAST_ACK
- [
TIME_WAIT -> CLOSED
] 客戶端要等待一個(gè)2MSL
(Max Segment Life
, 報(bào)文最大生存時(shí)間)的時(shí)間,才會(huì)進(jìn)入CLOSED
狀態(tài)
服務(wù)端狀態(tài)轉(zhuǎn)化:
- [
ESTABLISHED -> CLOSE_WAIT
] 當(dāng)客戶端主動(dòng)關(guān)閉連接(調(diào)用close),服務(wù)器會(huì)收到結(jié)束報(bào)文段,服務(wù)器返回確認(rèn)報(bào)文段并進(jìn)入CLOSE_WAIT
- [
CLOSE_WAIT -> LAST_ACK
] 進(jìn)入CLOSE_WAIT
后說(shuō)明服務(wù)器準(zhǔn)備關(guān)閉連接(需要處理完之前的數(shù)據(jù));當(dāng)服務(wù)器真正調(diào)用close關(guān)閉連接時(shí),會(huì)向客戶端發(fā)送FIN,此時(shí)服務(wù)器進(jìn)入LAST_ACK
狀態(tài),等待最后一個(gè)ACK到來(lái)(這個(gè)ACK是客戶端確認(rèn)收到了FIN) - [
LAST_ACK -> CLOSED
] 服務(wù)器收到了對(duì)FIN的ACK,徹底關(guān)閉連接
注:觸發(fā)四次揮手是上層雙方調(diào)用close(sock)
- 主動(dòng)斷開連接的一方,最終狀態(tài)是
TIME_WAIT
- 被動(dòng)斷開連接的一方,兩次揮手完成,進(jìn)入
CLOSE_WAIT
下面進(jìn)行做實(shí)驗(yàn),查看這兩個(gè)狀態(tài)
2.9.3 演示查看TIME_WAIT和CLOSE_WAIT狀態(tài)
代碼直接采用socket套接字TCP多線程版的,前面已經(jīng)講解過(guò)了,就不再解釋
初始化服務(wù)器initServer函數(shù)步驟大致如下:
- 調(diào)用socket函數(shù),創(chuàng)建套接字。
- 調(diào)用bind函數(shù),為服務(wù)端綁定一個(gè)端口號(hào)
- 調(diào)用listen函數(shù),將套接字設(shè)置為監(jiān)聽狀態(tài)
啟動(dòng)服務(wù)器start函數(shù)步驟大致如下:
- 調(diào)用accept函數(shù),獲取新鏈接
- 為客戶端提供服務(wù)
服務(wù)端提供的服務(wù),什么也不做,等待20秒服務(wù)端就直接退出即可,我們?cè)谶@20秒內(nèi)操作,操作就暈在客戶端連接好了之后,客戶端在20秒內(nèi)主動(dòng)退出即可
即演示的目的效果是:
- 客戶端主動(dòng)退出,最終狀態(tài)是
TIME_WAIT
- 服務(wù)端是被動(dòng)斷開連接的一方,被動(dòng)斷開連接的一方,兩次揮手完成,進(jìn)入
CLOSE_WAIT
tcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
static const int gbacklog = 5;
// 錯(cuò)誤類型枚舉
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
class tcpServer; // 聲明
class ThreadDate
{
public:
ThreadDate(tcpServer *self, int sockfd)
: _self(self), _sockfd(sockfd)
{}
public:
tcpServer *_self;
int _sockfd;
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _listensock(-1), _port(port)
{}
// 初始化服務(wù)器
void initServer()
{
// 1.創(chuàng)建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
// 2.綁定端口
// 2.1 填充 sockaddr_in 結(jié)構(gòu)體
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in結(jié)構(gòu)體全部初始化為0
local.sin_family = AF_INET; // 未來(lái)通信采用的是網(wǎng)絡(luò)通信
local.sin_port = htons(_port); // htons(_port)主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 綁定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要強(qiáng)轉(zhuǎn),(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
// 3. 把_listensock套接字設(shè)置為監(jiān)聽狀態(tài)
if (listen(_listensock, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
}
// 啟動(dòng)服務(wù)器
void start()
{
for (;;)
{
// 4. 獲取新鏈接,accept從_listensock套接字里面獲取新鏈接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 這里的sockfd才是真正為客戶端請(qǐng)求服務(wù)
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 獲取新鏈接失敗,但不會(huì)影響服務(wù)端運(yùn)行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 為sockfd提供服務(wù),即為客戶端提供服務(wù)
// 多線程版
pthread_t tid;
ThreadDate *td = new ThreadDate(this, sockfd);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 線程分離
ThreadDate *td = static_cast<ThreadDate *>(args);
td->_self->serviceIo(td->_sockfd);
close(td->_sockfd); // 必須關(guān)閉,由新線程關(guān)閉
delete td;
return nullptr;
}
// 提供服務(wù)
void serviceIo(int sockfd)
{
sleep(20); // 20秒之后線程關(guān)閉_sockfd,線程也退出
}
~tcpServer()
{}
private:
int _listensock; // listen套接字,不是用來(lái)數(shù)據(jù)通信的,是用來(lái)監(jiān)聽鏈接到來(lái)
uint16_t _port; // 端口號(hào)
};
tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
// 使用手冊(cè)
// ./tcpServer port
static void Uage(string proc)
{
cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uage(argv[0]);
exit(UAGE_ERR);
}
uint16_t port = atoi(argv[1]); // string to int
unique_ptr<tcpServer> tsvr(new tcpServer(port));
tsvr->initServer(); // 初始化服務(wù)器
tsvr->start(); // 啟動(dòng)服務(wù)器
return 0;
}
下面需要使用telnet命令,先介紹該命令
2.9.4 telnet命令
telnet命令
Telnet是一種用于遠(yuǎn)程登錄和管理網(wǎng)絡(luò)設(shè)備的協(xié)議,同時(shí)也可以用于測(cè)試網(wǎng)絡(luò)連接和端口的連通性。Telnet客戶端可以通過(guò)命令行或者圖形界面進(jìn)行操作。
在命令行中,可以使用telnet命令來(lái)連接到遠(yuǎn)程主機(jī)或者測(cè)試網(wǎng)絡(luò)連接。以下是使用telnet命令的一些常見用法:
1、連接到遠(yuǎn)程主機(jī):
telnet <hostname> [port]
是要連接的遠(yuǎn)程主機(jī)的域名或者IP地址,[port
]是要連接的端口,默認(rèn)為23(Telnet默認(rèn)端口)
例如,要連接到主機(jī)example.com
的Telnet服務(wù),可以使用以下命令:
telnet example.com
2、測(cè)試端口連通性:
telnet <hostname> <port>
<hostname
>是要測(cè)試的主機(jī)的域名或者IP地址,<port
>是要測(cè)試的端口號(hào)
例如,要測(cè)試主機(jī)example.com
的80
端口是否連通,可以使用以下命令:
telnet example.com 80
2、退出Telnet會(huì)話:
在Telnet會(huì)話中,可以使用以下命令退出:
quit
或者按下Ctrl+]
,然后輸入quit
。
4、安裝
如果Linux沒(méi)有安裝,先安裝telnet客戶端
yum install -y telnet
注:普通用戶需要sudo提權(quán)
2.9.5 演示
先運(yùn)行服務(wù)端,然后使用telnet命令連接服務(wù)端
注:由于沒(méi)有多余的機(jī)器,只在一臺(tái)機(jī)器下測(cè)試
打循環(huán)查看tcpServer:(查看服務(wù)端的)
while : ;do netstat -natp | grep tcpServer; sleep 1; echo "-----------------"; done
打循環(huán)查看telnet:(查看客戶端)
while : ;do netstat -natp | grep telnet; sleep 1; echo "-----------------"; done
打循環(huán)查看TIME_WAIT
:(查看客戶端)
while : ;do netstat -natp | grep TIME_WAIT; sleep 1; echo "-----------------"; done
打循環(huán)查看TIME_WAIT
是因?yàn)椋瑃elnet退出后查不到該進(jìn)程了
準(zhǔn)備工作完成,先運(yùn)行循環(huán),再啟動(dòng)服務(wù)端,再進(jìn)行telnet,客戶端要在20秒內(nèi)退出
0、運(yùn)行循環(huán)
1、啟動(dòng)服務(wù)端
2、telnet
3、telnet在20秒內(nèi)退出連接
服務(wù)端關(guān)閉sock,并退出
客戶端退出后,可以查到客戶端的TIME_WAIT
狀態(tài)
上述是我演示的是四次揮手的過(guò)程
- 主動(dòng)斷開連接的一方,最終狀態(tài)是
TIME_WAIT
- 被動(dòng)斷開連接的一方,兩次揮手完成,進(jìn)入
CLOSE_WAIT
TIME_WAIT
狀態(tài)持續(xù)一段時(shí)間后才進(jìn)入真正的關(guān)閉
TIME_WAIT的等待時(shí)長(zhǎng)是多少?
- 太長(zhǎng)的
TIME_WAIT
狀態(tài)會(huì)導(dǎo)致等待方維持連接的成本增加,浪費(fèi)資源。 - 太短的
TIME_WAIT
狀態(tài)可能無(wú)法保證ACK被對(duì)方接收,數(shù)據(jù)在網(wǎng)絡(luò)中消散。 - TCP協(xié)議規(guī)定,主動(dòng)關(guān)閉連接的一方在四次揮手后要進(jìn)入
TIME_WAIT
狀態(tài),等待兩個(gè)MSL
的時(shí)間才能進(jìn)入CLOSED
狀態(tài)。這樣可以確保連接的可靠關(guān)閉。
查看Linux的MSL
時(shí)間長(zhǎng)度:
cat /proc/sys/net/ipv4/tcp_fin_timeout
Centos7上默認(rèn)配置的值是60秒(可以修改)
為什么是
TIME_WAIT
的時(shí)間是2MSL
?
-
MSL
是TCP報(bào)文的最大生存時(shí)間, 因此TIME_WAIT
持續(xù)存在2*MSL
的話 - 就能保證在兩個(gè)傳輸方向上的尚未被接收或遲到的報(bào)文段都已經(jīng)消失(否則服務(wù)器立刻重啟,可能會(huì)收到來(lái)自上一個(gè)進(jìn)程的遲到的數(shù)據(jù),但是這種數(shù)據(jù)很可能是錯(cuò)誤的)
- 同時(shí)也是在理論上保證最后一個(gè)報(bào)文可靠到達(dá)(假設(shè)最后一個(gè)
ACK
丟失,那么服務(wù)器會(huì)再重發(fā)一個(gè)FIN
。這時(shí)雖然客戶端的進(jìn)程不在了,但是TCP連接還在,仍然可以重發(fā)FIN
)
如果服務(wù)器出現(xiàn)了大量的
CLOSE_WAIT
狀態(tài),說(shuō)明服務(wù)器:
- 服務(wù)器有bug,沒(méi)有做關(guān)閉文件符操作
close(sock)
- 服務(wù)器有壓力,可能服務(wù)端一直推送消息給客戶端,導(dǎo)致來(lái)不及close文件描述符
2.10 解決TIME_WAIT狀態(tài)引起的bind失敗的問(wèn)題
綁定失敗現(xiàn)象:
有客戶端連著服務(wù)端,服務(wù)端主動(dòng)退出(需要進(jìn)行重啟),緊接著服務(wù)端再次啟動(dòng)綁定相同的端口就會(huì)出現(xiàn)綁定失敗的現(xiàn)象
綁定失敗的原因:
- 服務(wù)端主動(dòng)斷開連接(服務(wù)端退出),最終狀態(tài)是
TIME_WAIT
-
TIME_WAIT
持續(xù)存在2*MSL
,即TIME_WAIT
狀態(tài)需要等待兩個(gè)MSL
的時(shí)間才能進(jìn)入CLOSED
狀態(tài) - 在
2*MSL
時(shí)間內(nèi),服務(wù)器綁定的端口還一直被占用 - 即服務(wù)端立馬退出,再進(jìn)行重啟,這時(shí)候就會(huì)存在綁定端口失敗的問(wèn)題
現(xiàn)象演示
代碼依舊是上面的,運(yùn)行服務(wù)器,客戶端進(jìn)行連接,然后服務(wù)端主動(dòng)退出,再進(jìn)行重啟就會(huì)出現(xiàn)綁定端口失敗
綁定端口失敗的危害
- 比如在某些場(chǎng)景下,618,雙11,這時(shí)候服務(wù)器的壓力會(huì)比較大,如果說(shuō)服務(wù)器崩潰了,需要立即馬上進(jìn)行重啟,并且能夠立馬重啟成功,如果在一分鐘內(nèi)或兩分鐘內(nèi)無(wú)法重啟成功,就會(huì)造成巨大的損失(金錢),618,雙11都是分秒必爭(zhēng)的
解決方法
使用setsockopt()
設(shè)置socket描述符的選項(xiàng)SO_REUSEADDR
為1,表示允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符
// 設(shè)置地址復(fù)用
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
修改代碼
在創(chuàng)建套接字后面設(shè)置即可
編譯運(yùn)行,再進(jìn)行測(cè)試,bind綁定失敗問(wèn)題沒(méi)有了
2.11 流量控制
這個(gè)在前面的16位窗口大小已經(jīng)談過(guò)一部分了(上一篇),這里再來(lái)詳細(xì)介紹。
接收端處理數(shù)據(jù)的速度是有限的,如果發(fā)送端發(fā)的太快,導(dǎo)致接收端的緩沖區(qū)被打滿,這個(gè)時(shí)候如果發(fā)送端繼續(xù)發(fā)送,就會(huì)造成丟包,繼而引起丟包重傳等等一系列連鎖反應(yīng)
因此TCP支持根據(jù)接收端的處理能力,來(lái)決定發(fā)送端的發(fā)送速度,這個(gè)機(jī)制就叫做流量控制(Flow Control)
- 接收端將自己可以接收的緩沖區(qū)大小放入 TCP 首部中的 “窗口大小” 字段,通過(guò)ACK端通知發(fā)送端
- 窗口大小字段越大,說(shuō)明網(wǎng)絡(luò)的吞吐量越高
- 接收端一旦發(fā)現(xiàn)自己的緩沖區(qū)快滿了,就會(huì)將窗口大小設(shè)置成一個(gè)更小的值通知給發(fā)送端
- 發(fā)送端接受到這個(gè)窗口之后,就會(huì)減慢自己的發(fā)送速度
- 如果接收端緩沖區(qū)滿了,就會(huì)將窗口置為0,這時(shí)發(fā)送方不再發(fā)送數(shù)據(jù),但是需要定期發(fā)送一個(gè)窗口探測(cè)數(shù)據(jù)段,使接收端把窗口大小告訴發(fā)送端
16位窗口大小:16位最大表示65535,那TCP窗口最大就是65535嗎?
- 理論上確實(shí)是這樣的,但實(shí)際上TCP報(bào)頭當(dāng)中40字節(jié)的選項(xiàng)字段中包含了一個(gè)窗口擴(kuò)大因子M,實(shí)際窗口大小是窗口字段的值左移M位得到的
第一次向?qū)Ψ桨l(fā)送數(shù)據(jù)時(shí)如何得知對(duì)方的窗口大?。?/p>
- 雙方在進(jìn)行TCP通信之前需要進(jìn)行三次握手建立連接。
- 在握手過(guò)程中,除了驗(yàn)證通信信道是否通暢,雙方還會(huì)交換其他信息,其中包括告知對(duì)方自己的接收能力。
- 這樣,在雙方正式開始通信之前,雙方已經(jīng)了解對(duì)方的接收數(shù)據(jù)能力。
- 因此,雙方在發(fā)送數(shù)據(jù)時(shí)可以根據(jù)對(duì)方的接收能力進(jìn)行調(diào)整,避免緩沖區(qū)溢出的問(wèn)題。
2.12 滑動(dòng)窗口
前面已經(jīng)提到過(guò)TCP的工作模式了(上一篇),TCP的工作模式有兩種
第一種串行發(fā)送數(shù)據(jù)(不是TCP真正的工作模式)
一發(fā)一收的方式性能較低(串行)
第二種并行發(fā)送數(shù)據(jù)(TCP真正的工作模式)
并行發(fā)送數(shù)據(jù),可以大大的提高性能(其實(shí)是將多個(gè)段的等待時(shí)間重疊在一起了)
第二種情況是TCP的真正工作模式,即主流,但是也會(huì)存在第一種工作模式,第一種情況是很少的,但也會(huì)存在
發(fā)送方可以一次發(fā)送多個(gè)報(bào)文給對(duì)方,此時(shí)也就意味著發(fā)送出去的這部分報(bào)文當(dāng)中有相當(dāng)一部分?jǐn)?shù)據(jù)是暫時(shí)沒(méi)有收到應(yīng)答的
發(fā)送緩沖區(qū)當(dāng)中的數(shù)據(jù)可以分為三部分:
- 已經(jīng)發(fā)送并且已經(jīng)收到ACK的數(shù)據(jù)。
- 已經(jīng)發(fā)送還但沒(méi)有收到ACK的數(shù)據(jù)。
- 還沒(méi)有發(fā)送的數(shù)據(jù)。
- 還有一個(gè)就是剩余空間,即沒(méi)有數(shù)據(jù)的空間,這個(gè)剩余空間可能有也可能沒(méi)有
發(fā)送緩沖區(qū)的第二部分就叫做滑動(dòng)窗口
窗口大小指的是無(wú)需等待確認(rèn)應(yīng)答而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值
- 下圖的窗口大小就是4000個(gè)字節(jié)(四個(gè)段)
- 發(fā)送前四個(gè)段的時(shí)候,不需要等待任何ACK,直接發(fā)送
- 收到第一個(gè)ACK后,滑動(dòng)窗口向后移動(dòng),繼續(xù)發(fā)送下一個(gè)段的數(shù)據(jù),依次類推
- 操作系統(tǒng)內(nèi)核為了維護(hù)這個(gè)滑動(dòng)窗口,需要開辟發(fā)送緩沖區(qū)來(lái)記錄當(dāng)前還有哪些數(shù)據(jù)沒(méi)有應(yīng)答,只有確認(rèn)應(yīng)答過(guò)的數(shù)據(jù),才能從緩沖區(qū)刪掉
- 窗口越大, 則網(wǎng)絡(luò)的吞吐率就越高
例如
- 1-1000、1001-2000數(shù)據(jù)接收端已經(jīng)收到了并且進(jìn)行了ACK,則1-1000、1001-2000的數(shù)據(jù)不在發(fā)送緩沖區(qū)的滑動(dòng)窗口里面了
- 現(xiàn)在連續(xù)發(fā)送2001-3000、3001-4000、4001-5000、5001-6001的四個(gè)段數(shù)據(jù),此時(shí)不需要等待任何ACK,可以直接進(jìn)行發(fā)送
- 當(dāng)收到對(duì)方應(yīng)答的確認(rèn)序號(hào)為3001時(shí),說(shuō)明2001-3000這個(gè)數(shù)據(jù)段已經(jīng)被對(duì)方收到了,此時(shí)該數(shù)據(jù)段應(yīng)該被歸入發(fā)送緩沖區(qū)當(dāng)中的已發(fā)送&&已收到ACK部分
- 而由于我們假設(shè)對(duì)方的窗口大小一直是4000,因此滑動(dòng)窗口現(xiàn)在可以向右移動(dòng),繼續(xù)發(fā)送6001-7000的數(shù)據(jù)段,以此類推
- TCP的重傳機(jī)制要求暫時(shí)保存發(fā)出但未收到確認(rèn)的數(shù)據(jù),這些數(shù)據(jù)實(shí)際上位于滑動(dòng)窗口中。
- 滑動(dòng)窗口的左側(cè)是已經(jīng)被對(duì)方可靠接收到的數(shù)據(jù),因此只有滑動(dòng)窗口左側(cè)的數(shù)據(jù)可以被覆蓋或刪除。
- 滑動(dòng)窗口除了限定可以直接發(fā)送的數(shù)據(jù),還支持TCP的重傳機(jī)制,即當(dāng)發(fā)送的數(shù)據(jù)未收到確認(rèn)時(shí),可以重新發(fā)送滑動(dòng)窗口中的數(shù)據(jù)。
- 這樣可以確保數(shù)據(jù)的可靠傳輸,提高通信的可靠性和效率。
發(fā)送緩沖區(qū)建模1:
- 緩沖區(qū)的本質(zhì)就是一個(gè)char類型的數(shù)組,而滑動(dòng)窗口就在發(fā)送緩沖區(qū)內(nèi),所以滑動(dòng)窗口本質(zhì)也是一個(gè)char類型的數(shù)組
- 滑動(dòng)窗口可以被看作是由兩個(gè)指針限定的一個(gè)范圍(指向數(shù)組下標(biāo))。比如,我們可以用
win_start
指針指向滑動(dòng)窗口的左側(cè),win_end
指針指向滑動(dòng)窗口的右側(cè)。 - 在
win_start
和win_end
之間的區(qū)間范圍內(nèi)的數(shù)據(jù)可以被稱為滑動(dòng)窗口。 - 通過(guò)移動(dòng)這兩個(gè)指針,可以實(shí)現(xiàn)滑動(dòng)窗口的滑動(dòng)和調(diào)整大小,以適應(yīng)不同的網(wǎng)絡(luò)條件和接收方的接收能力。這樣的設(shè)計(jì)可以有效地控制發(fā)送方的發(fā)送速率,并支持TCP的重傳機(jī)制,以確保數(shù)據(jù)的可靠傳輸。
當(dāng)發(fā)送端收到對(duì)方的ACK應(yīng)答時(shí),如果響應(yīng)當(dāng)中的確認(rèn)序號(hào)為xxx
,窗口大小為win
,此時(shí)就可以將win_start
更新為xxx
,而將win_end
更新為win_start+win
(暫時(shí)這樣理解,下面再詳細(xì)解釋)
滑動(dòng)窗口大小是怎么設(shè)定的??未來(lái)怎么變化?
- 滑動(dòng)窗口的大小是根據(jù)網(wǎng)絡(luò)條件和接收方的接收能力來(lái)設(shè)定的。通常,滑動(dòng)窗口的大小由發(fā)送方和接收方之間的協(xié)商來(lái)確定。
- 在建立TCP連接時(shí),雙方會(huì)通過(guò)握手過(guò)程交換窗口大小的信息。發(fā)送方通常會(huì)根據(jù)接收方的通知來(lái)設(shè)定初始的滑動(dòng)窗口大小。這個(gè)初始大小可以是固定的值,也可以是根據(jù)網(wǎng)絡(luò)條件動(dòng)態(tài)調(diào)整的值。
- 未來(lái),滑動(dòng)窗口的大小可以根據(jù)網(wǎng)絡(luò)條件和接收方的反饋信息進(jìn)行動(dòng)態(tài)變化。TCP協(xié)議中有一種叫做擁塞控制的機(jī)制,可以根據(jù)網(wǎng)絡(luò)擁塞的程度來(lái)調(diào)整滑動(dòng)窗口的大小。當(dāng)網(wǎng)絡(luò)擁塞時(shí),發(fā)送方會(huì)減小滑動(dòng)窗口的大小,以降低發(fā)送速率,從而減輕網(wǎng)絡(luò)負(fù)載。當(dāng)網(wǎng)絡(luò)狀況改善時(shí),發(fā)送方可以增大滑動(dòng)窗口的大小,以提高發(fā)送速率。(擁塞控制下面談)
總之,滑動(dòng)窗口的大小是根據(jù)網(wǎng)絡(luò)條件和接收方的接收能力來(lái)動(dòng)態(tài)設(shè)定和調(diào)整的,以實(shí)現(xiàn)更高效的數(shù)據(jù)傳輸和網(wǎng)絡(luò)擁塞控制。
16位窗口大小與滑動(dòng)窗口
- 16位窗口大小指的是TCP協(xié)議中的窗口字段的大小,它用16位二進(jìn)制數(shù)來(lái)表示。窗口字段表示接收方還能接收多少字節(jié)的數(shù)據(jù),用于控制發(fā)送方的發(fā)送速率。
- 滑動(dòng)窗口是一種數(shù)據(jù)傳輸?shù)臋C(jī)制,用于控制發(fā)送方發(fā)送數(shù)據(jù)的速率和接收方接收數(shù)據(jù)的能力?;瑒?dòng)窗口的大小可以根據(jù)網(wǎng)絡(luò)條件和接收方的接收能力進(jìn)行調(diào)整。
- 滑動(dòng)窗口的大小可以與16位窗口大小相關(guān)聯(lián)。發(fā)送方在發(fā)送數(shù)據(jù)時(shí),會(huì)根據(jù)接收方的窗口大小來(lái)確定發(fā)送窗口的大小。如果接收方的16位窗口大小為N,發(fā)送方可以將發(fā)送窗口的大小設(shè)置為N,以確保發(fā)送的數(shù)據(jù)不會(huì)超過(guò)接收方的窗口大小。
- 當(dāng)發(fā)送方發(fā)送了一段數(shù)據(jù)后,接收方會(huì)發(fā)送確認(rèn)消息(ACK)給發(fā)送方,同時(shí)更新窗口字段的值。發(fā)送方根據(jù)接收方的窗口字段的值來(lái)調(diào)整發(fā)送窗口的大小,以控制發(fā)送的數(shù)據(jù)量。
總之,16位窗口大小與滑動(dòng)窗口的關(guān)系是,發(fā)送方根據(jù)接收方的16位窗口大小來(lái)確定發(fā)送窗口的大小,并根據(jù)接收方的確認(rèn)消息來(lái)動(dòng)態(tài)調(diào)整發(fā)送窗口的大小,以實(shí)現(xiàn)數(shù)據(jù)的可靠傳輸和流量控制。
滑動(dòng)窗口會(huì)向左滑動(dòng)嗎?滑動(dòng)窗口整體一定會(huì)向右滑動(dòng)么?
- 一定不會(huì)向左滑動(dòng),滑動(dòng)窗口左邊的數(shù)據(jù)一定是已經(jīng)發(fā)送了并且是對(duì)方已經(jīng)收到了
- 在正常情況下,滑動(dòng)窗口整體是向右滑動(dòng)的,即發(fā)送方不斷發(fā)送新的數(shù)據(jù),接收方不斷確認(rèn)收到數(shù)據(jù),并將窗口向右滑動(dòng)。這樣可以實(shí)現(xiàn)高效的數(shù)據(jù)傳輸。
- 然而,在某些特殊情況下,滑動(dòng)窗口整體可能會(huì)不滑動(dòng),滑動(dòng)窗口還可能一直變小。比如,對(duì)方的接收緩沖區(qū)的上層不取數(shù)據(jù),緩沖區(qū)慢慢會(huì)被打滿,此時(shí)滑動(dòng)窗口的大小也慢慢變小,并且整體不會(huì)向右滑動(dòng)
滑動(dòng)窗口大小會(huì)一直不變嗎?會(huì)變小嗎?會(huì)變大嗎??
- 滑動(dòng)窗口的大小是根據(jù)網(wǎng)絡(luò)條件和接收方的接收能力來(lái)動(dòng)態(tài)設(shè)定和調(diào)整的,即一直會(huì)發(fā)生變化(也可能不變,不會(huì)是一直不變,如上面例子確認(rèn)3001、4001)
- 滑動(dòng)窗口可能會(huì)變小,上面的例子,確認(rèn)5001、6001,最小就是變?yōu)?
- 滑動(dòng)窗口可能會(huì)變大,比如,對(duì)方的接收緩沖區(qū)的上層一下子讀取了完了全部的數(shù)據(jù),此時(shí)接收緩沖區(qū)的接收能力就會(huì)變大,滑動(dòng)窗口也隨之變大,提高發(fā)送數(shù)據(jù)的速率
丟包問(wèn)題
當(dāng)發(fā)送端一次發(fā)送多個(gè)報(bào)文數(shù)據(jù)時(shí),此時(shí)的丟包情況也可以分為兩種。
情況一: 數(shù)據(jù)包已經(jīng)抵達(dá),ACK丟包。
- 部分ACK丟包并不要緊,此時(shí)可以通過(guò)后續(xù)的ACK進(jìn)行確認(rèn)
- 在這種情況下,由于接收方的ACK對(duì)于2001-3000和4001-5000的數(shù)據(jù)包丟失了,發(fā)送方無(wú)法直接知道這些數(shù)據(jù)包是否已經(jīng)成功到達(dá)。但是,發(fā)送方可以通過(guò)接收到的最后一個(gè)有效ACK(5001-6000數(shù)據(jù)包的確認(rèn))來(lái)推斷一些信息。
- TCP協(xié)議的規(guī)定,收到一個(gè)確認(rèn)ACK序號(hào)為6001的確認(rèn)消息表示接收方成功接收了序號(hào)為1-6000的字節(jié)數(shù)據(jù)。因此,發(fā)送方可以推測(cè)在丟失的這一段(2001-3000和4001-5000)之前的數(shù)據(jù)包也應(yīng)該已經(jīng)到達(dá)了接收方。
情況二: 數(shù)據(jù)包真的丟了。
- 當(dāng)發(fā)送端連續(xù)收到三次確認(rèn)序號(hào)為1001的響應(yīng)報(bào)文,即接收方對(duì)于1001-2000的數(shù)據(jù)包的確認(rèn)被重復(fù)確認(rèn)了三次,發(fā)送端會(huì)認(rèn)為這些數(shù)據(jù)包丟失了。
- TCP協(xié)議的重傳機(jī)制,當(dāng)發(fā)送端連續(xù)收到三次相同的確認(rèn)序號(hào)時(shí),會(huì)觸發(fā)快速重傳。在這種情況下,發(fā)送端會(huì)立即重傳1001-2000的數(shù)據(jù)包,以確保接收方能夠正確接收到這些數(shù)據(jù)。
- 通過(guò)快速重傳,發(fā)送端可以快速恢復(fù)丟失的數(shù)據(jù)包,提高數(shù)據(jù)的可靠性和傳輸效率。
- 因此,在這個(gè)特殊情況下,滑動(dòng)窗口的整體位置不會(huì)發(fā)生變化,但發(fā)送端會(huì)根據(jù)丟失的數(shù)據(jù)包觸發(fā)快速重傳機(jī)制,重新發(fā)送丟失的數(shù)據(jù)包。
TCP協(xié)議的重傳機(jī)制,當(dāng)發(fā)送端連續(xù)收到三次相同的確認(rèn)序號(hào)時(shí)(觸發(fā)重傳機(jī)制),會(huì)觸發(fā)高速重發(fā)控制,也叫快重傳
發(fā)送緩沖區(qū)建模2:如果滑動(dòng)窗口一直向后滑動(dòng),空間大小不夠了怎么辦??
- 發(fā)送緩沖區(qū)被內(nèi)核組織稱為了一種環(huán)形結(jié)構(gòu)(本質(zhì)依舊是插入類型的線性數(shù)組),通過(guò)數(shù)據(jù)下標(biāo)進(jìn)行模運(yùn)算實(shí)現(xiàn)
- 環(huán)形結(jié)構(gòu)怎么滑動(dòng)也不會(huì)發(fā)生越界問(wèn)題
快重傳 VS 超時(shí)重傳
- 快速重傳和超時(shí)重傳是TCP協(xié)議中兩種常用的重傳機(jī)制,用于處理丟失的數(shù)據(jù)包。它們的主要區(qū)別在于觸發(fā)重傳的條件和重傳的時(shí)機(jī)。
- 快速重傳(
Fast Retransmit
)是指當(dāng)發(fā)送方連續(xù)收到三個(gè)重復(fù)的確認(rèn)序號(hào)時(shí),即接收方對(duì)同一個(gè)數(shù)據(jù)包的確認(rèn)被重復(fù)確認(rèn)了三次,發(fā)送方會(huì)立即重傳該數(shù)據(jù)包。這是因?yàn)檫B續(xù)收到重復(fù)確認(rèn)序號(hào)通常意味著該數(shù)據(jù)包已經(jīng)丟失了,為了快速恢復(fù)丟失的數(shù)據(jù)包,發(fā)送方會(huì)觸發(fā)快速重傳機(jī)制,立即重傳該數(shù)據(jù)包,而不必等待超時(shí)重傳。 - 超時(shí)重傳(
Timeout Retransmission
)是指當(dāng)發(fā)送方發(fā)送一個(gè)數(shù)據(jù)包后,等待一段時(shí)間(超時(shí)時(shí)間)后仍未收到對(duì)應(yīng)的確認(rèn)消息時(shí),發(fā)送方會(huì)認(rèn)為該數(shù)據(jù)包丟失了,會(huì)觸發(fā)超時(shí)重傳機(jī)制,重新發(fā)送該數(shù)據(jù)包。超時(shí)時(shí)間是根據(jù)網(wǎng)絡(luò)狀況和往返時(shí)間動(dòng)態(tài)調(diào)整的,如果網(wǎng)絡(luò)延遲較高或丟包較多,超時(shí)時(shí)間會(huì)相應(yīng)增加。 - 快速重傳和超時(shí)重傳都是為了處理丟失的數(shù)據(jù)包,但觸發(fā)條件和重傳時(shí)機(jī)不同。快速重傳是基于連續(xù)收到重復(fù)確認(rèn)序號(hào)觸發(fā)的,可以更快地恢復(fù)丟失的數(shù)據(jù)包,減少了等待超時(shí)的時(shí)間。而超時(shí)重傳是基于超時(shí)時(shí)間觸發(fā)的,適用于網(wǎng)絡(luò)延遲較高或丟包較多的情況。
綜上所述,快速重傳和超時(shí)重傳是TCP協(xié)議中兩種常用的重傳機(jī)制,根據(jù)不同的情況選擇合適的重傳策略,以提高數(shù)據(jù)的可靠傳輸性能。
以上話題都是端到端,客戶端到服務(wù)端,服務(wù)端到客戶端,沒(méi)有考慮有網(wǎng)絡(luò)的,TCP也有網(wǎng)絡(luò)問(wèn)題方面的機(jī)制控制,就是擁塞控制。
2.13 擁塞控制
為什么會(huì)有擁塞控制?
- 兩個(gè)主機(jī)在進(jìn)行TCP通信的過(guò)程中,偶爾出現(xiàn)個(gè)別數(shù)據(jù)包丟失的情況是很正常的,此時(shí)可以通過(guò)快速重傳或超時(shí)重傳來(lái)補(bǔ)發(fā)丟失的數(shù)據(jù)包。(TCP認(rèn)為是自己的問(wèn)題)
- 但如果雙方在通信時(shí)出現(xiàn)大量數(shù)據(jù)包丟失的情況,這就不再是正?,F(xiàn)象了。(TCP認(rèn)為不是自己的問(wèn)題)
舉個(gè)例子:
- 比如,高數(shù)考試,一個(gè)班有30人,考試下來(lái)全班只有張三一個(gè)人掛了,張三認(rèn)為這是自己的問(wèn)題
- 那如果考試下來(lái),全班只有張三一個(gè)人通過(guò)了考試,其他全掛了,這時(shí)其他人認(rèn)為不是自己的問(wèn)題也不是張三的問(wèn)題,而是試卷的問(wèn)題
- 同理TCP也是如此
- 比如,客戶端發(fā)送了10000個(gè)報(bào)文,服務(wù)端收到了9999個(gè),只丟了一個(gè)報(bào)文,TCP認(rèn)為這是自己的問(wèn)題
- 如果服務(wù)端只收到了10個(gè),丟了9990個(gè)報(bào)文,這時(shí)TCP就不會(huì)再認(rèn)為是自己的問(wèn)題(即不會(huì)進(jìn)行重傳報(bào)文),而是網(wǎng)絡(luò)的問(wèn)題
所以,TCP不僅考慮了通信雙端主機(jī)的問(wèn)題,同時(shí)也考慮了網(wǎng)絡(luò)的問(wèn)題。
- 擁塞窗口:考慮雙方通信時(shí)網(wǎng)絡(luò)的問(wèn)題,如果發(fā)送的數(shù)據(jù)超過(guò)了擁塞窗口的大小,可能引起網(wǎng)絡(luò)擁塞。在雙方網(wǎng)絡(luò)通信時(shí),偶爾出現(xiàn)少量的丟包是允許的,但一旦出現(xiàn)大量的丟包,這就是量變引起質(zhì)變,此時(shí)TCP不再假設(shè)是雙方接收和發(fā)送數(shù)據(jù)的問(wèn)題,而是判斷雙方通信信道網(wǎng)絡(luò)出現(xiàn)了擁塞問(wèn)題。
- 網(wǎng)絡(luò)擁塞:是指在計(jì)算機(jī)網(wǎng)絡(luò)中,當(dāng)網(wǎng)絡(luò)中的數(shù)據(jù)流量超過(guò)網(wǎng)絡(luò)鏈路或節(jié)點(diǎn)的處理能力時(shí),導(dǎo)致網(wǎng)絡(luò)性能下降、延遲增加、丟包率增加等現(xiàn)象。網(wǎng)絡(luò)擁塞通常發(fā)生在網(wǎng)絡(luò)的瓶頸點(diǎn),即網(wǎng)絡(luò)中的某些關(guān)鍵節(jié)點(diǎn)或鏈路無(wú)法處理大量的數(shù)據(jù)流量。
從另一個(gè)視角看待
- 如果出現(xiàn)了網(wǎng)絡(luò)擁塞問(wèn)題,網(wǎng)絡(luò)擁塞不僅僅影響單個(gè)主機(jī),幾乎會(huì)影響網(wǎng)絡(luò)中的所有主機(jī)(其他主機(jī)也是會(huì)發(fā)生大量丟包)。
- 所以,出現(xiàn)了網(wǎng)絡(luò)擁塞之后,雙方主機(jī)可以減少數(shù)據(jù)傳輸?shù)乃俾?,盡量少發(fā)數(shù)據(jù)甚至不發(fā)數(shù)據(jù),等待網(wǎng)絡(luò)狀況恢復(fù)后再逐漸恢復(fù)數(shù)據(jù)傳輸速率(減少網(wǎng)絡(luò)負(fù)擔(dān))
- 如果出現(xiàn)了網(wǎng)絡(luò)擁塞,雙發(fā)出現(xiàn)了大量的丟包,雙方的主機(jī)還進(jìn)行重傳報(bào)文的,只會(huì)給網(wǎng)絡(luò)雪上加霜,網(wǎng)絡(luò)上其他主機(jī)也是如此(雪崩的時(shí)候沒(méi)有一片雪花是無(wú)辜的)
- 因此,所有使用TCP傳輸控制協(xié)議的主機(jī)都需要執(zhí)行擁塞避免算法(擁塞控制),以統(tǒng)一減少發(fā)送窗口的大小,從而避免網(wǎng)絡(luò)擁塞問(wèn)題的進(jìn)一步惡化和傳播。
- 因此,擁塞控制雖然看似只是針對(duì)單個(gè)主機(jī)的通信策略,但實(shí)際上是所有主機(jī)在網(wǎng)絡(luò)崩潰后都應(yīng)該遵守的策略。
- 當(dāng)網(wǎng)絡(luò)發(fā)生擁塞時(shí),所有主機(jī)都需要執(zhí)行擁塞避免算法,這樣才能有效地緩解網(wǎng)絡(luò)擁塞問(wèn)題,防止雪崩效應(yīng)的發(fā)生,并盡快恢復(fù)網(wǎng)絡(luò)的正常運(yùn)行。
擁塞控制
雖然TCP有了滑動(dòng)窗口這個(gè)大殺器,能夠高效可靠的發(fā)送大量的數(shù)據(jù)。但是如果在剛開始階段就發(fā)送大量的數(shù)據(jù),仍然可能引發(fā)問(wèn)題
因?yàn)榫W(wǎng)絡(luò)上有很多的計(jì)算機(jī),可能當(dāng)前的網(wǎng)絡(luò)狀態(tài)就已經(jīng)比較擁堵,在不清楚當(dāng)前網(wǎng)絡(luò)狀態(tài)下,貿(mào)然發(fā)送大量的數(shù)據(jù),是很有可能引起雪上加霜
TCP引入 慢啟動(dòng)機(jī)制,先發(fā)少量的數(shù)據(jù),探探路,摸清當(dāng)前的網(wǎng)絡(luò)擁堵狀態(tài),再?zèng)Q定按照多大的速度傳輸數(shù)據(jù)
擁塞窗口:
- 發(fā)送開始的時(shí)候,定義擁塞窗口大小為1
- 每次收到一個(gè)ACK應(yīng)答,擁塞窗口加1
- 每次發(fā)送數(shù)據(jù)包的時(shí)候,將擁塞窗口和接收端主機(jī)反饋的窗口大小做比較,取較小的值作為實(shí)際發(fā)送的窗口
即自己的滑動(dòng)窗口的大小 = min(擁塞窗口,對(duì)端窗口大小),兩者取較小值
- 滑動(dòng)窗口:自己的
- 擁塞窗口:網(wǎng)絡(luò)的
- 窗口大?。簩?duì)端的,對(duì)端的接收能力
像上面這樣的擁塞窗口增長(zhǎng)速度,是指數(shù)級(jí)別的, “慢啟動(dòng)” 只是指初使時(shí)慢,但是增長(zhǎng)速度非???/p>
- 為了不增長(zhǎng)的那么快,因此不能使擁塞窗口單純的加倍
- 此處引入一個(gè)叫做慢啟動(dòng)的閾值
- 當(dāng)擁塞窗口超過(guò)這個(gè)閾值的時(shí)候,不再按照指數(shù)方式增長(zhǎng),而是按照線性方式增長(zhǎng)
- 當(dāng)TCP開始啟動(dòng)的時(shí)候,慢啟動(dòng)閾值等于對(duì)端窗口的最大值
- 在每次超時(shí)重發(fā)的時(shí)候,慢啟動(dòng)閾值會(huì)變成原來(lái)的一半,同時(shí)擁塞窗口置回1
- 指數(shù)增長(zhǎng)。剛開始進(jìn)行TCP通信時(shí)擁塞窗口的值為1,并不斷按指數(shù)的方式進(jìn)行增長(zhǎng)
- 加法增大。慢啟動(dòng)的閾值初始時(shí)為對(duì)方窗口大小的最大值,圖中慢啟動(dòng)閾值的初始值為16,因此當(dāng)擁塞窗口的值增大到16時(shí)就不再按指數(shù)形式增長(zhǎng)了,而變成了的線性增長(zhǎng)。
- 乘法減小。擁塞窗口在線性增長(zhǎng)的過(guò)程中,在增大到24時(shí)如果發(fā)生了網(wǎng)絡(luò)擁塞,此時(shí)慢啟動(dòng)的閾值將變?yōu)楫?dāng)前擁塞窗口的一半,也就是12,并且擁塞窗口的值被重新設(shè)置為1,所以下一次擁塞窗口由指數(shù)增長(zhǎng)變?yōu)榫€性增長(zhǎng)時(shí)擁塞窗口的值應(yīng)該是12。
少量的丟包,我們僅僅是觸發(fā)超時(shí)重傳;大量的丟包,我們就認(rèn)為網(wǎng)絡(luò)擁塞。
當(dāng)TCP通信開始后,網(wǎng)絡(luò)吞吐量會(huì)逐漸上升;隨著網(wǎng)絡(luò)發(fā)生擁堵,吞吐量會(huì)立刻下降。
擁塞控制,歸根結(jié)底是TCP協(xié)議想盡可能快的把數(shù)據(jù)傳輸給對(duì)方,但是又要避免給網(wǎng)絡(luò)造成太大壓力的折中方案。(擁塞控制也是為了保證可靠性和傳輸速率)
2.14 延遲應(yīng)答
如果接收數(shù)據(jù)的主機(jī)立刻返回ACK應(yīng)答, 這時(shí)候返回的窗口可能比較小
如果接收數(shù)據(jù)的主機(jī)收到數(shù)據(jù)后立即進(jìn)行ACK應(yīng)答,此時(shí)返回的窗口可能比較小。
- 假設(shè)對(duì)方接收端緩沖區(qū)剩余空間大小為1M,對(duì)方一次收到500K的數(shù)據(jù)后,如果立即進(jìn)行ACK應(yīng)答,此時(shí)返回的窗口就是500K。
- 但實(shí)際接收端處理數(shù)據(jù)的速度很快,10ms之內(nèi)就將接收緩沖區(qū)中500K的數(shù)據(jù)消費(fèi)掉了。
- 在這種情況下,接收端處理還遠(yuǎn)沒(méi)有達(dá)到自己的極限,即使窗口再放大一些,也能處理過(guò)來(lái)。
- 如果接收端稍微等一會(huì)再進(jìn)行ACK應(yīng)答,比如等待200ms再應(yīng)答,那么這時(shí)返回的窗口大小就是1M。
延遲應(yīng)答的目的不是為了保證可靠性,而是為了提高數(shù)據(jù)的傳輸效率。(保證在網(wǎng)絡(luò)不擁塞的情況下盡量提高傳輸效率)
那么所有的報(bào)文都可以延遲應(yīng)答么?
答案肯定也不是,延遲應(yīng)答會(huì)有以下兩個(gè)限制:
- 數(shù)量限制:每隔N個(gè)包就應(yīng)答一次
- 時(shí)間限制:超過(guò)最大延遲時(shí)間就應(yīng)答一次(不能比超時(shí)重傳的時(shí)間長(zhǎng))
具體的數(shù)量和超時(shí)時(shí)間,依操作系統(tǒng)不同也有差異;一般N
取2
,超時(shí)時(shí)間取200ms
2.15 捎帶應(yīng)答
- 主機(jī)A給主機(jī)B發(fā)送了一條消息,當(dāng)主機(jī)B收到這條消息后需要對(duì)其進(jìn)行ACK應(yīng)答,但如果主機(jī)B此時(shí)正好也要給主機(jī)A發(fā)生消息,此時(shí)這個(gè)ACK就可以搭順風(fēng)車,而不用單獨(dú)發(fā)送一個(gè)ACK應(yīng)答,此時(shí)主機(jī)B發(fā)送的這個(gè)報(bào)文既發(fā)送了數(shù)據(jù),又完成了對(duì)收到數(shù)據(jù)的響應(yīng)
- 這就是捎帶應(yīng)答,捎帶應(yīng)答也是為了提高傳輸效率
TCP連接不直接保證可靠性,但是會(huì)間接保證可靠性
- TCP連接確實(shí)不直接保證可靠性,但它通過(guò)一系列機(jī)制間接保證了數(shù)據(jù)的可靠傳輸。
- 比如:序號(hào)和確認(rèn)機(jī)制、滑動(dòng)窗口、擁塞控制、流量控制等,這些機(jī)制是直接保障可靠性,
- 這些機(jī)制建立的基礎(chǔ)是已經(jīng)連接已經(jīng)建立成功,所以TCP連接確實(shí)不直接保證可靠性,而是間接保證可靠性
以上便是TCP所有的策略
2.16 面向字節(jié)流
當(dāng)創(chuàng)建一個(gè)TCP的socket時(shí),同時(shí)在內(nèi)核中會(huì)創(chuàng)建一個(gè)發(fā)送緩沖區(qū)和一個(gè)接收緩沖區(qū)。
- 調(diào)用write函數(shù)就可以將數(shù)據(jù)寫入發(fā)送緩沖區(qū)中,此時(shí)write函數(shù)就可以進(jìn)行返回了,接下來(lái)發(fā)送緩沖區(qū)當(dāng)中的數(shù)據(jù)就是由TCP自行進(jìn)行發(fā)送的。
- 如果發(fā)送的字節(jié)數(shù)太長(zhǎng),TCP會(huì)將其拆分成多個(gè)數(shù)據(jù)包發(fā)出。如果發(fā)送的字節(jié)數(shù)太短,TCP可能會(huì)先將其留在發(fā)送緩沖區(qū)當(dāng)中,等到合適的時(shí)機(jī)再進(jìn)行發(fā)送。
- 接收數(shù)據(jù)的時(shí)候,數(shù)據(jù)也是從網(wǎng)卡驅(qū)動(dòng)程序到達(dá)內(nèi)核的接收緩沖區(qū),可以通過(guò)調(diào)用read函數(shù)來(lái)讀取接收緩沖區(qū)當(dāng)中的數(shù)據(jù)。
- 而調(diào)用read函數(shù)讀取接收緩沖區(qū)中的數(shù)據(jù)時(shí),也可以按任意字節(jié)數(shù)進(jìn)行讀取。
這個(gè)緩沖區(qū)在前面已經(jīng)詳細(xì)談過(guò)了,這里就不展開說(shuō)了
由于緩沖區(qū)的存在,TCP程序的讀和寫不需要一一匹配(面向字節(jié)流),例如:
寫100個(gè)字節(jié)數(shù)據(jù)時(shí),可以調(diào)用一次write寫100字節(jié),也可以調(diào)用100次write,每次寫一個(gè)字節(jié)。
讀100個(gè)字節(jié)數(shù)據(jù)時(shí),也完全不需要考慮寫的時(shí)候是怎么寫的,既可以一次read100個(gè)字節(jié),也可以一次read一個(gè)字節(jié),重復(fù)100次。
于TCP來(lái)說(shuō),它并不關(guān)心發(fā)送緩沖區(qū)當(dāng)中的是什么數(shù)據(jù),在TCP看來(lái)這些只是一個(gè)個(gè)的字節(jié)數(shù)據(jù),它的任務(wù)就是將這些數(shù)據(jù)準(zhǔn)確無(wú)誤的發(fā)送到對(duì)方的接收緩沖區(qū)當(dāng)中就行了,而至于如何解釋這些數(shù)據(jù)完全由上層應(yīng)用來(lái)決定,這就叫做面向字節(jié)流
比對(duì)面向數(shù)據(jù)報(bào)
- 應(yīng)用層交付給UDP多長(zhǎng)的報(bào)文,UDP就原樣發(fā)送,既不會(huì)拆分,也不會(huì)合并,這就叫做面向數(shù)據(jù)報(bào)
- 比如用UDP傳輸100個(gè)字節(jié)的數(shù)據(jù),發(fā)送端調(diào)用一次發(fā)送函數(shù),發(fā)送100字節(jié),那么接收端也必須調(diào)用對(duì)應(yīng)的一次接收函數(shù),接收100個(gè)字節(jié);如果發(fā)送端調(diào)用十次發(fā)送函數(shù),則接收端也必須調(diào)用對(duì)應(yīng)的十次接收函數(shù),即UDP協(xié)議,發(fā)送函數(shù)的次數(shù) : 接收函數(shù)的次數(shù) =
1 : 1
2.17 粘包問(wèn)題
什么是粘包?(基于TCP的應(yīng)用層問(wèn)題)
- 首先要明確,粘包問(wèn)題中的“包”,是指的應(yīng)用層的數(shù)據(jù)包。
- 在TCP的協(xié)議頭中,沒(méi)有如同UDP一樣的“報(bào)文長(zhǎng)度”這樣的字段。
- 站在傳輸層的角度,TCP是一個(gè)一個(gè)報(bào)文過(guò)來(lái)的,按照序號(hào)排好序放在緩沖區(qū)中。
- 但站在應(yīng)用層的角度,看到的只是一串連續(xù)的字節(jié)數(shù)據(jù)。
- 那么應(yīng)用程序看到了這么一連串的字節(jié)數(shù)據(jù),就不知道從哪個(gè)部分開始到哪個(gè)部分,是一個(gè)完整的應(yīng)用層數(shù)據(jù)包。
如何解決粘包問(wèn)題
要解決粘包問(wèn)題,本質(zhì)就是要明確報(bào)文和報(bào)文之間的邊界。
- 對(duì)于定長(zhǎng)的包,保證每次都按固定大小讀取即可。
- 對(duì)于變長(zhǎng)的包,可以在報(bào)頭的位置,約定一個(gè)包總長(zhǎng)度的字段,從而就知道了包的結(jié)束位置。
- 對(duì)于變長(zhǎng)的包,還可以在包和包之間使用明確的分隔符。因?yàn)閼?yīng)用層協(xié)議是程序員自己來(lái)定的,只要保證分隔符不和正文沖突即可。
對(duì)于UDP協(xié)議來(lái)說(shuō),是否也存在 “粘包問(wèn)題” ?
- 對(duì)于UDP,如果還沒(méi)有上層交付數(shù)據(jù),UDP的報(bào)文長(zhǎng)度仍然在,同時(shí),UDP是一個(gè)一個(gè)把數(shù)據(jù)交付給應(yīng)用層的,有很明確的數(shù)據(jù)邊界。
- 站在應(yīng)用層的角度,使用UDP的時(shí)候,要么收到完整的UDP報(bào)文,要么不收,不會(huì)出現(xiàn)“半個(gè)”的情況。
因此UDP是不存在粘包問(wèn)題的,根本原因就是UDP報(bào)頭當(dāng)中的16位UDP長(zhǎng)度記錄的UDP報(bào)文的長(zhǎng)度,因此UDP在底層的時(shí)候就把報(bào)文和報(bào)文之間的邊界明確了,而TCP存在粘包問(wèn)題就是因?yàn)門CP是面向字節(jié)流的,TCP報(bào)文之間沒(méi)有明確的邊界。
2.18 TCP異常情況
(1)進(jìn)程終止
當(dāng)客戶端與服務(wù)端已經(jīng)建立好連接了,如果客戶端進(jìn)程突然終止,此時(shí)建立好的連接會(huì)怎么樣?
- 當(dāng)一個(gè)進(jìn)程退出時(shí),該進(jìn)程曾經(jīng)打開的文件描述符都會(huì)自動(dòng)關(guān)閉,因此當(dāng)客戶端進(jìn)程退出時(shí),相當(dāng)于自動(dòng)調(diào)用了close函數(shù)關(guān)閉了對(duì)應(yīng)的文件描述符,此時(shí)雙方操作系統(tǒng)在底層會(huì)正常完成四次揮手,然后釋放對(duì)應(yīng)的連接資源。
- 也就是說(shuō),進(jìn)程終止時(shí)會(huì)釋放文件描述符,TCP底層仍然可以發(fā)送FIN,和進(jìn)程正常退出沒(méi)有區(qū)別。
(2)機(jī)器重啟
當(dāng)客戶端與服務(wù)端已經(jīng)建立好連接了:
- 選擇重啟主機(jī)時(shí),操作系統(tǒng)會(huì)先殺掉所有進(jìn)程然后再進(jìn)行關(guān)機(jī)重啟,因此機(jī)器重啟和進(jìn)程終止的情況是一樣的,此時(shí)雙方操作系統(tǒng)也會(huì)正常完成四次揮手,然后釋放對(duì)應(yīng)的連接資源。
(3)機(jī)器掉電(斷電源)/網(wǎng)線斷開
當(dāng)客戶端與服務(wù)端已經(jīng)建立好連接了:一端突然斷電或斷網(wǎng)了
比如是客戶端斷電或斷網(wǎng)后,服務(wù)器端在短時(shí)間內(nèi)無(wú)法知道客戶端掉線了,因此在服務(wù)器端會(huì)維持與客戶端建立的連接,但這個(gè)連接也不會(huì)一直維持,因?yàn)門CP是有?;畈呗缘?。
- 服務(wù)器會(huì)定期發(fā)送保活探測(cè)報(bào)文給客戶端,以檢測(cè)客戶端的存在狀況。如果連續(xù)多次都沒(méi)有收到客戶端的應(yīng)答,服務(wù)器就會(huì)認(rèn)為客戶端已經(jīng)掉線,并關(guān)閉這條連接。
- 此外,客戶端也可以定期向服務(wù)器發(fā)送心跳消息,以確保服務(wù)器知道自己的存在。如果服務(wù)器長(zhǎng)時(shí)間沒(méi)有收到客戶端的心跳消息,也會(huì)認(rèn)為客戶端已經(jīng)掉線,并關(guān)閉對(duì)應(yīng)的連接。
- 綜上所述,TCP通過(guò)?;钐綔y(cè)和心跳消息機(jī)制,間接地檢測(cè)客戶端的在線狀態(tài),并在一定時(shí)間內(nèi)關(guān)閉掉線的連接,以保證連接的有效性和資源的合理利用。
此外,應(yīng)用層的某些協(xié)議,也有一些類似的檢測(cè)機(jī)制,例如基于長(zhǎng)連接的HTTP,也會(huì)定期檢測(cè)對(duì)方的存在狀態(tài)。
2.19 TCP小結(jié)
為什么TCP這么復(fù)雜?
因?yàn)橐WC可靠性,同時(shí)又盡可能的提高性能
可靠性:
- 檢驗(yàn)和
- 序列號(hào)
- 確認(rèn)應(yīng)答
- 超時(shí)重傳
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動(dòng)窗口
- 快速重傳
- 延遲應(yīng)答
- 捎帶應(yīng)答
除此之外,還有一些定時(shí)器(超時(shí)重傳定時(shí)器, ?;疃〞r(shí)器, TIME_WAIT定時(shí)器等)
2.20 基于TCP應(yīng)用層協(xié)議
常見的基于TCP的應(yīng)用層協(xié)議如下:
- HTTP(超文本傳輸協(xié)議)
- HTTPS(安全數(shù)據(jù)傳輸協(xié)議)
- SSH(安全外殼協(xié)議)
- Telnet(遠(yuǎn)程終端協(xié)議)
- FTP(文件傳輸協(xié)議)
- SMTP(電子郵件傳輸協(xié)議)
當(dāng)然,也包括你自己寫TCP程序時(shí)自定義的應(yīng)用層協(xié)議
2.21 TCP/UDP對(duì)比
TCP是可靠連接, 那么是不是TCP一定就優(yōu)于UDP呢?
TCP和UDP之間的優(yōu)點(diǎn)和缺點(diǎn),不能簡(jiǎn)單絕對(duì)的進(jìn)行比較,不存在誰(shuí)好誰(shuí)不好的問(wèn)題,他們只是應(yīng)用場(chǎng)景不同:
- TCP用于可靠傳輸?shù)那闆r,應(yīng)用于文件傳輸,重要狀態(tài)更新等場(chǎng)景
- UDP用于對(duì)高速傳輸和實(shí)時(shí)性要求較高的通信領(lǐng)域。例如,早期的QQ,視頻傳輸?shù)?,另外UDP可以用于廣播
歸根結(jié)底,TCP和UDP都是程序員的工具,什么時(shí)機(jī)用,具體怎么用,還是要根據(jù)具體的需求場(chǎng)景去判定
如何用UDP實(shí)現(xiàn)可靠傳輸
參考TCP的可靠性機(jī)制,在應(yīng)用層實(shí)現(xiàn)類似的邏輯,例如:
- 引入序列號(hào), 保證數(shù)據(jù)順序
- 引入確認(rèn)應(yīng)答,確保對(duì)端收到了數(shù)據(jù)
- 引入超時(shí)重傳,如果隔一段時(shí)間沒(méi)有應(yīng)答,就重發(fā)數(shù)據(jù)
- …
三、TCP實(shí)驗(yàn):理解listen的第二個(gè)參數(shù)
listen函數(shù)的作用是設(shè)置套接字為監(jiān)聽狀態(tài),該函數(shù)的第二個(gè)參數(shù)之前沒(méi)有談,現(xiàn)在來(lái)談一下
第二個(gè)參數(shù)backlog:全連接隊(duì)列的最大長(zhǎng)度。
如果有多個(gè)客戶端同時(shí)發(fā)來(lái)連接請(qǐng)求,此時(shí)未被服務(wù)器處理的連接就會(huì)放入連接隊(duì)列,該參數(shù)代表的就是這個(gè)全連接隊(duì)列的最大長(zhǎng)度,一般不能設(shè)置太大
下面進(jìn)行做實(shí)驗(yàn):
該實(shí)驗(yàn)不進(jìn)行accept獲取_listensock
套接字新連接,什么也不干,只進(jìn)行監(jiān)聽連接的到來(lái),backlog
設(shè)置為2
tcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
static const int gbacklog = 2; // 全連接隊(duì)列大小
// 錯(cuò)誤類型枚舉
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _listensock(-1), _port(port)
{}
// 初始化服務(wù)器
void initServer()
{
// 1.創(chuàng)建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
// 1.1 設(shè)置地址復(fù)用
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2.綁定端口
// 2.1 填充 sockaddr_in 結(jié)構(gòu)體
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in結(jié)構(gòu)體全部初始化為0
local.sin_family = AF_INET; // 未來(lái)通信采用的是網(wǎng)絡(luò)通信
local.sin_port = htons(_port); // htons(_port)主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 綁定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要強(qiáng)轉(zhuǎn),(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
// 3. 把_listensock套接字設(shè)置為監(jiān)聽狀態(tài)
if (listen(_listensock, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
}
// 啟動(dòng)服務(wù)器
void start()
{
for (;;)
{
sleep(1); // 什么也不做,不從_listensock套接字里面獲取新連接
}
}
~tcpServer()
{}
private:
int _listensock; // listen套接字,不是用來(lái)數(shù)據(jù)通信的,是用來(lái)監(jiān)聽鏈接到來(lái)
uint16_t _port; // 端口號(hào)
};
tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
// 使用手冊(cè)
// ./tcpServer port
static void Uage(string proc)
{
cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uage(argv[0]);
exit(UAGE_ERR);
}
uint16_t port = atoi(argv[1]); // string to int
unique_ptr<tcpServer> tsvr(new tcpServer(port));
tsvr->initServer(); // 初始化服務(wù)器
tsvr->start(); // 啟動(dòng)服務(wù)器
return 0;
}
編譯運(yùn)行服務(wù)器,此時(shí)啟動(dòng) 3 個(gè)客戶端同時(shí)連接服務(wù)器, 用 netstat 查看服務(wù)器狀態(tài), 一切正常
但是啟動(dòng)第四個(gè)客戶端時(shí), 發(fā)現(xiàn)服務(wù)器對(duì)于第四個(gè)連接的狀態(tài)存在問(wèn)題了
客戶端狀態(tài)正常, 但是服務(wù)器端出現(xiàn)了SYN_RECV
狀態(tài), 而不是ESTABLISHED
狀態(tài)
這是因?yàn)? Linux內(nèi)核協(xié)議棧為一個(gè)tcp連接管理使用兩個(gè)隊(duì)列:
- 半連接隊(duì)列(用來(lái)保存處于
SYN_SENT
和SYN_RECV
狀態(tài)的請(qǐng)求) - 全連接隊(duì)列(accpetd隊(duì)列)(用來(lái)保存處
established
狀態(tài),但是應(yīng)用層沒(méi)有調(diào)用accept取走的請(qǐng)求)
這個(gè)全連接隊(duì)列不能太長(zhǎng),也不能沒(méi)有:
- 如果全連接隊(duì)列過(guò)長(zhǎng),會(huì)導(dǎo)致服務(wù)器資源的浪費(fèi)。每個(gè)連接請(qǐng)求都會(huì)占用一定的內(nèi)存和其他資源,如果隊(duì)列過(guò)長(zhǎng),服務(wù)器可能無(wú)法及時(shí)處理所有的連接請(qǐng)求,導(dǎo)致資源耗盡和服務(wù)質(zhì)量下降
- 相反,如果全連接隊(duì)列沒(méi)有長(zhǎng)度,也會(huì)導(dǎo)致問(wèn)題。如果服務(wù)器無(wú)法及時(shí)處理所有的連接請(qǐng)求,就會(huì)導(dǎo)致連接請(qǐng)求被丟棄,客戶端無(wú)法建立連接。這會(huì)導(dǎo)致客戶端的請(qǐng)求失敗和連接超時(shí),影響用戶體驗(yàn)和服務(wù)可用性。
而全連接隊(duì)列的長(zhǎng)度會(huì)受到 listen 第二個(gè)參數(shù)的影響文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-618965.html
- 全連接隊(duì)列滿了的時(shí)候,就無(wú)法繼續(xù)讓當(dāng)前連接的狀態(tài)進(jìn)入 established 狀態(tài)了
- 這個(gè)全連接隊(duì)列的長(zhǎng)度通過(guò)上述實(shí)驗(yàn)可知,全連接的長(zhǎng)度是 listen 的第二個(gè)參數(shù) + 1
上述實(shí)驗(yàn),我們?cè)O(shè)置的全連接隊(duì)列大小是2,前三次連接正常,但是到了第四次連接的處于半鏈接隊(duì)列,處于了SYN_RECV
狀態(tài)
但是在客戶端看來(lái),連接已經(jīng)建立好了,但是在服務(wù)端看來(lái)沒(méi)有建立連接成功,因?yàn)榉?wù)端對(duì)于第三次握手的ACK進(jìn)行了忽略
TCP內(nèi)容真多,終于完結(jié)了,TCP寫了差不多三萬(wàn)字
--------------------- END ----------------------文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-618965.html
「 作者 」 楓葉先生
「 更新 」 2023.7.30
「 聲明 」 余之才疏學(xué)淺,故所撰文疏漏難免,
或有謬誤或不準(zhǔn)確之處,敬請(qǐng)讀者批評(píng)指正。
到了這里,關(guān)于「網(wǎng)絡(luò)編程」傳輸層協(xié)議_ TCP協(xié)議學(xué)習(xí)_及原理深入理解(二 - 完結(jié))[萬(wàn)字詳解]的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!