目錄
1、協(xié)議的通俗理解
1.1 理解協(xié)議
2.應(yīng)用層
2.1 http協(xié)議
2.2 HTTP的方法
?2.3 HTTP的狀態(tài)碼
2.4 HTTP常見(jiàn)Header
3、傳輸層
3.1 端口號(hào)
3.1.1 端口號(hào)范圍劃分
3.1.2 netstat
3.1.3 認(rèn)識(shí)知名端口號(hào)(Well-Know Port Number)
3.2 UDP協(xié)議
3.2.1 UDP協(xié)議端格式
3.2.2 UDP的特點(diǎn)
3.2.3 基于UDP的應(yīng)用層協(xié)議
3.3 TCP協(xié)議
3.3.1 TCP協(xié)議段格式
3.3.2 確認(rèn)應(yīng)答(ACK)機(jī)制
3.3.3 超時(shí)重傳機(jī)制
3.3.4 連接管理機(jī)制
3.3.5 滑動(dòng)窗口
3.3.6 流量控制
3.3.7 擁塞控制
3.3.8 延遲應(yīng)答
3.3.9 捎帶應(yīng)答
3.4 面向字節(jié)流
3.4.1 粘包問(wèn)題
4.5 TCP異常情況
1、協(xié)議的通俗理解
??????? 在之前的幾篇文章中,我們一起看過(guò)一些關(guān)于linux中關(guān)于網(wǎng)絡(luò)的系統(tǒng)調(diào)用,就已經(jīng)可以寫(xiě)一個(gè)簡(jiǎn)易的服務(wù)器了,
??????? 那我們?cè)趧e人面前提起網(wǎng)絡(luò),都會(huì)聽(tīng)到一個(gè)字眼叫做協(xié)議,在網(wǎng)絡(luò)中的每一層中,都有協(xié)議,在上一篇文章中,我們也說(shuō)過(guò),協(xié)議就是兩臺(tái)主機(jī)在通信的時(shí)候,約定好的通信格式,多方都遵守這個(gè)通信格式來(lái)通信的話,就可以實(shí)現(xiàn)正確通信,可以拿到對(duì)方發(fā)給我的正確信息。
????????我們可以模擬一個(gè)協(xié)議,對(duì)協(xié)議進(jìn)行一個(gè)通俗一點(diǎn)的理解
1.1 理解協(xié)議
我們可以寫(xiě)一個(gè)服務(wù)器版本的計(jì)算器,
??????? 由于呢,在服務(wù)器端和用戶(hù)端之間進(jìn)行通信,存在有一些問(wèn)題,所以我們對(duì)問(wèn)題先進(jìn)行研究,
????????用戶(hù)端發(fā)起請(qǐng)求,要計(jì)算這個(gè)式子的值,那用戶(hù)端是不是要以某種格式發(fā)送,因?yàn)閿?shù)據(jù)在網(wǎng)絡(luò)中都是以字符串的形式進(jìn)行傳輸?shù)?,那用?hù)端向服務(wù)器端發(fā)送的數(shù)據(jù)格式是不是要被服務(wù)器端知道,這樣才能解析出正確的式子,
??????? 服務(wù)器端計(jì)算完之后,將結(jié)果返回給用戶(hù)端的時(shí)候,是不是也要以某種形式進(jìn)行組織,客戶(hù)端才能將數(shù)據(jù)進(jìn)行正確解析。
所以,客戶(hù)端就和服務(wù)器端形成一下約定:
??????? (1)客戶(hù)端發(fā)起的請(qǐng)求數(shù)據(jù)必須用空格將數(shù)據(jù)隔開(kāi),比如"x + y",x和+之間、+和y之間,必須存在一個(gè)空格,方便服務(wù)器端進(jìn)行解析計(jì)算,
??????? (2)服務(wù)器端返回的數(shù)據(jù)中,須存在計(jì)算狀態(tài)碼,若請(qǐng)求式子合理且可以進(jìn)行運(yùn)算,狀態(tài)碼置為0,否則置為1,并且,狀態(tài)碼和計(jì)算結(jié)果中間也要用空格隔開(kāi),如"0 10";
??????? 如上,這就是一個(gè)小小的協(xié)議約定,只不過(guò)是我們自己約定的,根據(jù)我們要設(shè)計(jì)的計(jì)算器服務(wù)定制的一套協(xié)議。
下面是關(guān)于服務(wù)器版本計(jì)算器的核心代碼:
void calcullator(int serviseSock)
{
std::string inbuffer;
while (1)
{
// 接收字符流,寫(xiě)入inbuffer緩沖區(qū)中,
if (!cal::cal_server::Recv(serviseSock, inbuffer))
{
break;
}
// 進(jìn)行反序列化,拿到運(yùn)算符和運(yùn)算數(shù)
std::size_t left = str.find(SPACE);
if (left == std::string::npos){
return false;
}
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos){
std::cout << "輸入格式有誤" << std::endl;
return ;
}
int _x = atoi(str.substr(0,left).c_str());
int _y = atoi(str.substr(right + SPACE_LEN).c_str());
if(left + SPACE_LEN > str.size()) {
std::cout << "輸入格式有誤" << std::endl;
return;
}
char _op = str[left + SPACE_LEN];
// 進(jìn)行運(yùn)算,使用code接收返回碼
int code = 0;
int result = calculate(_x,_y,_op,&code);
// 進(jìn)行序列化
std::string str;
str += std::to_string(code);
str += SPACE;
str += std::to_string(result);
return str;
//進(jìn)行發(fā)送
cal::cal_server::Send(serviseSock, str);
}
}
??我們測(cè)試發(fā)現(xiàn),這樣是可以運(yùn)行的,但是,這個(gè)程序其實(shí)還是存在漏洞的:
如果說(shuō),這個(gè)服務(wù)器被訪問(wèn)的很頻繁,那么服務(wù)器端在進(jìn)行讀取的時(shí)候,就會(huì)有問(wèn)題,這是因?yàn)樵诜?wù)器所調(diào)用的系統(tǒng)接口底層,他會(huì)有一個(gè)收發(fā)緩沖區(qū),如果請(qǐng)求過(guò)多,造成了堆積,那么上面寫(xiě)的代碼就會(huì)導(dǎo)致數(shù)據(jù)的粘包問(wèn)題,就是一個(gè)用戶(hù)的請(qǐng)求和下一個(gè)用戶(hù)的請(qǐng)求粘合了,讀取太多,或者上一次讀取太多,導(dǎo)致這一次只讀取了一半,比如:"1+255-44100+" 、"200100" 、"-100"。
所以我們需要在制定一個(gè)協(xié)議:
??????? 我們需要在原本協(xié)議約定的基礎(chǔ)上再加上能夠區(qū)分一個(gè)報(bào)文是否為完整報(bào)文的信息:
??????? "length/nx + y/n" 、"length/ncode result/n"
例如:"9/n100 + 100/n" 、 "5/n0 200/n"
????????length代表這個(gè)報(bào)文的長(zhǎng)度,兩個(gè)換行符中間的是報(bào)文,
??????? 如果在報(bào)文解析的時(shí)候,格式不符合協(xié)議約定,那么就需要重新讀取緩沖區(qū)里的數(shù)據(jù),不多拿,不少拿
??????? 下來(lái)我們進(jìn)行修改實(shí)現(xiàn)
void calcullator(int serviseSock)
{
std::string inbuffer;
while (1)
{
// 接收字符流
if (!cal::cal_server::Recv(serviseSock, inbuffer))
{
break;
}
// 分析字符流,如果沒(méi)有拿到完整報(bào)文,就對(duì)緩沖區(qū)不作修改,
// 如果緩沖區(qū)內(nèi)不知一份報(bào)文,就只拿一份進(jìn)行處理
std::string package = Decode(inbuffer);
if (package.empty())
{
continue;
}
// 拿到了一個(gè)完整的報(bào)文
// 進(jìn)行反序列化
request req;
req.Deserialize(package);
// 進(jìn)行運(yùn)算
response res = calculate(req);
// 進(jìn)行序列化
std::string res_string = res.Serialize();
res_string = Encode(res_string);
cal::cal_server::Send(serviseSock, res_string);
}
}
?進(jìn)行報(bào)文解析封裝和包裝的方法:
#include <iostream>
#include <string>
#include <cstring>
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\n"
#define SEP_LEN strlen(SEP)
std::string Decode(std::string & buffer) {
size_t pos = buffer.find(SEP);
if (pos == std::string::npos){
return "";
}
int len = atoi(buffer.substr(0, pos).c_str());
int surplus = buffer.size() - pos - 2 * SEP_LEN;
if (surplus >= len) {
//說(shuō)明有完整的報(bào)文
buffer.erase(0,pos + SEP_LEN);
std::string str = buffer.substr(0,len);
buffer.erase(0,len + SEP_LEN);
return str;
}
return "";
}
std::string Encode(std::string & s) {
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
class request{
public:
request(){}
request(int x,int y,char op):_x(x),_y(y),_op(op) {}
~request(){}
std::string Serialize() {
std::string str;
str += std::to_string(_x);
str += SPACE;
str += _op;
str += SPACE;
str += std::to_string(_y);
return str;
}
bool Deserialize(std::string &str) {
std::size_t left = str.find(SPACE);
if (left == std::string::npos){
return false;
}
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos){
return false;
}
_x = atoi(str.substr(0,left).c_str());
_y = atoi(str.substr(right + SPACE_LEN).c_str());
if(left + SPACE_LEN > str.size()) {
return false;
}
_op = str[left + SPACE_LEN];
return true;
}
public:
int _x;
int _y;
char _op;
};
class response{
public:
response(){}
response(int code, int result):_code(code),_result(result) {}
~response(){}
std::string Serialize() {
std::string str;
str += std::to_string(_code);
str += SPACE;
str += std::to_string(_result);
return str;
}
bool Deserialize(std::string &str) {
std::size_t pos = str.find(SPACE);
if (pos == std::string::npos){
return false;
}
_code = atoi(str.substr(0,pos).c_str());
_result = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
}
public:
int _code;
int _result;
};
? 相信通過(guò)上面的例子,我們就對(duì)協(xié)議就有了稍微深刻一點(diǎn)的理解了。
2.應(yīng)用層
2.1 http協(xié)議
??????? 在我們上網(wǎng)的時(shí)候,瀏覽器地址框內(nèi)的地址前面,都會(huì)有http://或者h(yuǎn)ttps://的字符,這個(gè)就是我們?cè)趹?yīng)用層中的一些協(xié)議了,就和上面我們舉的例子差不多
認(rèn)識(shí)URL
????????平時(shí)我們俗稱(chēng)的 "網(wǎng)址" 其實(shí)就是說(shuō)的 URL
下面為了方便探討http協(xié)議,我們可以寫(xiě)一個(gè)簡(jiǎn)易的http服務(wù)器,用以獲取http的請(qǐng)求:
void httpServerRequest(int serviseSock)
{
std::string inbuffer;
if (!HttpServer::Recv(serviseSock, inbuffer))
{
exit(10);
}
std::cout << inbuffer << std::endl;
}
運(yùn)行起來(lái):
使用瀏覽器訪問(wèn)我們所寫(xiě)的服務(wù)器:
這樣我們就獲取了http的一個(gè)請(qǐng)求:
我們還可以通過(guò)抓包工具抓取我們剛剛的請(qǐng)求:
http請(qǐng)求:
????????首行: [方法] + [url] + [版本]
????????Header: 請(qǐng)求的屬性, 冒號(hào)分割的鍵值對(duì);每組屬性之間使用\n分隔;遇到空行表示Header部分結(jié)束
????????Body: 空行后面的內(nèi)容都是Body. Body允許為空字符串. 如果Body存在, 則在Header中會(huì)有一個(gè)Content-Length屬性來(lái)標(biāo)識(shí)Body的長(zhǎng)度;????????
HTTP響應(yīng)
????????首行: [版本號(hào)] + [狀態(tài)碼] + [狀態(tài)碼解釋]
????????Header: 請(qǐng)求的屬性, 冒號(hào)分割的鍵值對(duì);每組屬性之間使用\n分隔;遇到空行表示Header部分結(jié)束
????????Body: 空行后面的內(nèi)容都是Body. Body允許為空字符串. 如果Body存在, 則在Header中會(huì)有一個(gè)
????????Content-Length屬性來(lái)標(biāo)識(shí)Body的長(zhǎng)度; 如果服務(wù)器返回了一個(gè)html頁(yè)面, 那么html頁(yè)面內(nèi)容就是在body中.
2.2 HTTP的方法
?2.3 HTTP的狀態(tài)碼
????????最常見(jiàn)的狀態(tài)碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
2.4 HTTP常見(jiàn)Header
Content-Type: 數(shù)據(jù)類(lèi)型(text/html等)
Content-Length: Body的長(zhǎng)度
Host: 客戶(hù)端告知服務(wù)器, 所請(qǐng)求的資源是在哪個(gè)主機(jī)的哪個(gè)端口上;
User-Agent: 聲明用戶(hù)的操作系統(tǒng)和瀏覽器版本信息;
referer: 當(dāng)前頁(yè)面是從哪個(gè)頁(yè)面跳轉(zhuǎn)過(guò)來(lái)的;
location: 搭配3xx狀態(tài)碼使用, 告訴客戶(hù)端接下來(lái)要去哪里訪問(wèn);
Cookie: 用于在客戶(hù)端存儲(chǔ)少量信息. 通常用于實(shí)現(xiàn)會(huì)話(session)的功能;?
3、傳輸層
負(fù)責(zé)數(shù)據(jù)能夠從發(fā)送端傳輸接收端.
3.1 端口號(hào)
端口號(hào)(Port)標(biāo)識(shí)了一個(gè)主機(jī)上進(jìn)行通信的不同的應(yīng)用程序;
3.1.1 端口號(hào)范圍劃分
????????0 - 1023: 知名端口號(hào), HTTP, FTP, SSH等這些廣為使用的應(yīng)用層協(xié)議, 他們的端口號(hào)都是固定的.
????????1024 - 65535: 操作系統(tǒng)動(dòng)態(tài)分配的端口號(hào). 客戶(hù)端程序的端口號(hào), 就是由操作系統(tǒng)從這個(gè)范圍分配的.
????????在TCP/IP協(xié)議中, 用 "源IP", "源端口號(hào)", "目的IP", "目的端口號(hào)", "協(xié)議號(hào)" 這樣一個(gè)五元組來(lái)標(biāo)識(shí)一個(gè)通信(可以通過(guò)netstat -n查看);
3.1.2 netstat
netstat是一個(gè)用來(lái)查看網(wǎng)絡(luò)狀態(tài)的重要工具.
語(yǔ)法:netstat [選項(xiàng)]
功能:查看網(wǎng)絡(luò)狀態(tài)
常用選項(xiàng):
????????n 拒絕顯示別名,能顯示數(shù)字的全部轉(zhuǎn)化成數(shù)字
????????l 僅列出有在 Listen (監(jiān)聽(tīng)) 的服務(wù)狀態(tài)
????????p 顯示建立相關(guān)鏈接的程序名
????????t (tcp)僅顯示tcp相關(guān)選項(xiàng)
????????u (udp)僅顯示udp相關(guān)選項(xiàng)
????????a (all)顯示所有選項(xiàng),默認(rèn)不顯示LISTEN相關(guān)
pidof
在查看服務(wù)器的進(jìn)程id時(shí)非常方便.
語(yǔ)法:pidof [進(jìn)程名]
功能:通過(guò)進(jìn)程名, 查看進(jìn)程id
3.1.3 認(rèn)識(shí)知名端口號(hào)(Well-Know Port Number)
????????有些服務(wù)器是非常常用的, 為了使用方便, 人們約定一些常用的服務(wù)器, 都是用以下這些固定的端口號(hào):
????????ssh服務(wù)器, 使用22端口
????????ftp服務(wù)器, 使用21端口
????????telnet服務(wù)器, 使用23端口
????????http服務(wù)器, 使用80端口
????????https服務(wù)器, 使用443
執(zhí)行下面的命令, 可以看到知名端口號(hào)
cat /etc/services
3.2 UDP協(xié)議
3.2.1 UDP協(xié)議端格式
16位UDP長(zhǎng)度, 表示整個(gè)數(shù)據(jù)報(bào)(UDP首部+UDP數(shù)據(jù))的最大長(zhǎng)度;
如果校驗(yàn)和出錯(cuò), 就會(huì)直接丟棄;?
3.2.2 UDP的特點(diǎn)
UDP傳輸?shù)倪^(guò)程類(lèi)似于寄信.
????????無(wú)連接: 知道對(duì)端的IP和端口號(hào)就直接進(jìn)行傳輸, 不需要建立連接;
????????不可靠: 沒(méi)有確認(rèn)機(jī)制, 沒(méi)有重傳機(jī)制; 如果因?yàn)榫W(wǎng)絡(luò)故障該段無(wú)法發(fā)到對(duì)方, UDP協(xié)議層也不會(huì)給應(yīng)用層返回任何錯(cuò)誤信息;
????????面向數(shù)據(jù)報(bào): 不能夠靈活的控制讀寫(xiě)數(shù)據(jù)的次數(shù)和數(shù)量;
面向數(shù)據(jù)報(bào)
????????應(yīng)用層交給UDP多長(zhǎng)的報(bào)文, UDP原樣發(fā)送, 既不會(huì)拆分, 也不會(huì)合并;
????????用UDP傳輸100個(gè)字節(jié)的數(shù)據(jù):
????????如果發(fā)送端調(diào)用一次sendto, 發(fā)送100個(gè)字節(jié), 那么接收端也必須調(diào)用對(duì)應(yīng)的一次recvfrom, 接收100個(gè)字節(jié); 而不能循環(huán)調(diào)用10次recvfrom, 每次接收10個(gè)字節(jié);
UDP的緩沖區(qū)
????????UDP沒(méi)有真正意義上的? 發(fā)送緩沖區(qū). 調(diào)用sendto會(huì)直接交給內(nèi)核, 由內(nèi)核將數(shù)據(jù)傳給網(wǎng)絡(luò)層協(xié)議進(jìn)行后續(xù)的傳輸動(dòng)作;
????????UDP具有接收緩沖區(qū). 但是這個(gè)接收緩沖區(qū)不能保證收到的UDP報(bào)的順序和發(fā)送UDP報(bào)的順序一致; 如果緩沖區(qū)滿了, 再到達(dá)的UDP數(shù)據(jù)就會(huì)被丟棄;
UDP的socket既能讀, 也能寫(xiě), 這個(gè)概念叫做? 全雙工
UDP使用注意事項(xiàng)
????????我們注意到, UDP協(xié)議首部中有一個(gè)16位的最大長(zhǎng)度. 也就是說(shuō)一個(gè)UDP能傳輸?shù)臄?shù)據(jù)最大長(zhǎng)度是64K(包含UDP首部).
????????然而64K在當(dāng)今的互聯(lián)網(wǎng)環(huán)境下, 是一個(gè)非常小的數(shù)字.
????????如果我們需要傳輸?shù)臄?shù)據(jù)超過(guò)64K, 就需要在應(yīng)用層手動(dòng)的分包, 多次發(fā)送, 并在接收端手動(dòng)拼裝;
3.2.3 基于UDP的應(yīng)用層協(xié)議
NFS: 網(wǎng)絡(luò)文件系統(tǒng)
TFTP: 簡(jiǎn)單文件傳輸協(xié)議
DHCP: 動(dòng)態(tài)主機(jī)配置協(xié)議
BOOTP: 啟動(dòng)協(xié)議(用于無(wú)盤(pán)設(shè)備啟動(dòng))
DNS: 域名解析協(xié)議
當(dāng)然, 也包括你自己寫(xiě)UDP程序時(shí)自定義的應(yīng)用層協(xié)議;
3.3 TCP協(xié)議
????????TCP全稱(chēng)為 "傳輸控制協(xié)議(Transmission Control Protocol"). 人如其名, 要對(duì)數(shù)據(jù)的傳輸進(jìn)行一個(gè)詳細(xì)的控制;
3.3.1 TCP協(xié)議段格式
源/目的端口號(hào): 表示數(shù)據(jù)是從哪個(gè)進(jìn)程來(lái), 到哪個(gè)進(jìn)程去;
????????4位TCP報(bào)頭長(zhǎng)度: 表示該TCP頭部有多少個(gè)32位bit(有多少個(gè)4字節(jié)); 所以TCP頭部最大長(zhǎng)度是15 * 4 = 60
6位標(biāo)志位:
????????URG: 緊急指針是否有效
????????ACK: 確認(rèn)號(hào)是否有效
????????PSH: 提示接收端應(yīng)用程序立刻從TCP緩沖區(qū)把數(shù)據(jù)讀走
????????RST: 對(duì)方要求重新建立連接; 我們把攜帶RST標(biāo)識(shí)的稱(chēng)為復(fù)位報(bào)文段
????????SYN: 請(qǐng)求建立連接; 我們把攜帶SYN標(biāo)識(shí)的稱(chēng)為同步報(bào)文段
????????FIN: 通知對(duì)方, 本端要關(guān)閉了, 我們稱(chēng)攜帶FIN標(biāo)識(shí)的為結(jié)束報(bào)文段
16位校驗(yàn)和: 發(fā)送端填充, CRC校驗(yàn). 接收端校驗(yàn)不通過(guò), 則認(rèn)為數(shù)據(jù)有問(wèn)題. 此處的檢驗(yàn)和不光包含TCP首部, 也包含TCP數(shù)據(jù)部分.
16位緊急指針: 標(biāo)識(shí)哪部分?jǐn)?shù)據(jù)是緊急數(shù)據(jù);
3.3.2 確認(rèn)應(yīng)答(ACK)機(jī)制
TCP將每個(gè)字節(jié)的數(shù)據(jù)都進(jìn)行了編號(hào). 即為序列號(hào).
????????每一個(gè)ACK都帶有對(duì)應(yīng)的確認(rèn)序列號(hào), 意思是告訴發(fā)送者, 我已經(jīng)收到了哪些數(shù)據(jù); 下一次你從哪里開(kāi)始發(fā).
3.3.3 超時(shí)重傳機(jī)制
主機(jī)A發(fā)送數(shù)據(jù)給B之后, 可能因?yàn)榫W(wǎng)絡(luò)擁堵等原因, 數(shù)據(jù)無(wú)法到達(dá)主機(jī)B;
如果主機(jī)A在一個(gè)特定時(shí)間間隔內(nèi)沒(méi)有收到B發(fā)來(lái)的確認(rèn)應(yīng)答, 就會(huì)進(jìn)行重發(fā);
但是, 主機(jī)A未收到B發(fā)來(lái)的確認(rèn)應(yīng)答, 也可能是因?yàn)锳CK丟失了;
因此主機(jī)B會(huì)收到很多重復(fù)數(shù)據(jù). 那么TCP協(xié)議需要能夠識(shí)別出那些包是重復(fù)的包, 并且把重復(fù)的丟棄掉.這時(shí)候我們可以利用前面提到的序列號(hào), 就可以很容易做到去重的效果.
最理想的情況下, 找到一個(gè)最小的時(shí)間, 保證 "確認(rèn)應(yīng)答一定能在這個(gè)時(shí)間內(nèi)返回".
但是這個(gè)時(shí)間的長(zhǎng)短, 隨著網(wǎng)絡(luò)環(huán)境的不同, 是有差異的.
如果超時(shí)時(shí)間設(shè)的太長(zhǎng), 會(huì)影響整體的重傳效率;
如果超時(shí)時(shí)間設(shè)的太短, 有可能會(huì)頻繁發(fā)送重復(fù)的包;
TCP為了保證無(wú)論在任何環(huán)境下都能比較高性能的通信, 因此會(huì)動(dòng)態(tài)計(jì)算這個(gè)最大超時(shí)時(shí)間.
Linux中(BSD Unix和Windows也是如此), 超時(shí)以500ms為一個(gè)單位進(jìn)行控制, 每次判定超時(shí)重發(fā)的超時(shí)時(shí)間都是500ms的整數(shù)倍.
如果重發(fā)一次之后, 仍然得不到應(yīng)答, 等待 2*500ms 后再進(jìn)行重傳.
如果仍然得不到應(yīng)答, 等待 4*500ms 進(jìn)行重傳. 依次類(lèi)推, 以指數(shù)形式遞增.
累計(jì)到一定的重傳次數(shù), TCP認(rèn)為網(wǎng)絡(luò)或者對(duì)端主機(jī)出現(xiàn)異常, 強(qiáng)制關(guān)閉連接.
3.3.4 連接管理機(jī)制
在正常情況下, TCP要經(jīng)過(guò)三次握手建立連接, 四次揮手?jǐn)嚅_(kāi)連接
服務(wù)端狀態(tài)轉(zhuǎn)化:
[CLOSED -> LISTEN] 服務(wù)器端調(diào)用listen后進(jìn)入LISTEN狀態(tài), 等待客戶(hù)端連接;
[LISTEN -> SYN_RCVD] 一旦監(jiān)聽(tīng)到連接請(qǐng)求(同步報(bào)文段), 就將該連接放入內(nèi)核等待隊(duì)列中, 并向客戶(hù)端發(fā)送SYN確認(rèn)報(bào)文.
[SYN_RCVD -> ESTABLISHED] 服務(wù)端一旦收到客戶(hù)端的確認(rèn)報(bào)文, 就進(jìn)入ESTABLISHED狀態(tài), 可以進(jìn)行讀寫(xiě)數(shù)據(jù)了.
[ESTABLISHED -> CLOSE_WAIT] 當(dāng)客戶(hù)端主動(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ì)向客戶(hù)端發(fā)送FIN, 此時(shí)服務(wù)器進(jìn)入LAST_ACK狀態(tài), 等待最后一個(gè)ACK到來(lái)(這個(gè)ACK是客戶(hù)端確認(rèn)收到了FIN)
[LAST_ACK -> CLOSED] 服務(wù)器收到了對(duì)FIN的ACK, 徹底關(guān)閉連接.
客戶(hù)端狀態(tài)轉(zhuǎn)化:
[CLOSED -> SYN_SENT] 客戶(hù)端調(diào)用connect, 發(fā)送同步報(bào)文段;
[SYN_SENT -> ESTABLISHED] connect調(diào)用成功, 則進(jìn)入ESTABLISHED狀態(tài), 開(kāi)始讀寫(xiě)數(shù)據(jù);
[ESTABLISHED -> FIN_WAIT_1] 客戶(hù)端主動(dòng)調(diào)用close時(shí), 向服務(wù)器發(fā)送結(jié)束報(bào)文段, 同時(shí)進(jìn)入
FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客戶(hù)端收到服務(wù)器對(duì)結(jié)束報(bào)文段的確認(rèn), 則進(jìn)入FIN_WAIT_2, 開(kāi)始等待服務(wù)器的結(jié)束報(bào)文段;
[FIN_WAIT_2 -> TIME_WAIT] 客戶(hù)端收到服務(wù)器發(fā)來(lái)的結(jié)束報(bào)文段, 進(jìn)入TIME_WAIT, 并發(fā)出LAST_ACK;
[TIME_WAIT -> CLOSED] 客戶(hù)端要等待一個(gè)2MSL(Max Segment Life, 報(bào)文最大生存時(shí)間)的時(shí)間, 才會(huì)進(jìn)入CLOSED狀態(tài).
3.3.5 滑動(dòng)窗口
收到第一個(gè)ACK后, 滑動(dòng)窗口向后移動(dòng), 繼續(xù)發(fā)送第五個(gè)段的數(shù)據(jù); 依次類(lèi)推;
操作系統(tǒng)內(nèi)核為了維護(hù)這個(gè)滑動(dòng)窗口, 需要開(kāi)辟? 發(fā)送緩沖區(qū)? 來(lái)記錄當(dāng)前還有哪些數(shù)據(jù)沒(méi)有應(yīng)答; 只有確認(rèn)應(yīng)答過(guò)的數(shù)據(jù), 才能從緩沖區(qū)刪掉;
窗口越大, 則網(wǎng)絡(luò)的吞吐率就越高;
那么如果出現(xiàn)了丟包, 如何進(jìn)行重傳? 這里分兩種情況討論.
情況一: 數(shù)據(jù)包已經(jīng)抵達(dá), ACK被丟了.
這種情況下, 部分ACK丟了并不要緊, 因?yàn)榭梢酝ㄟ^(guò)后續(xù)的ACK進(jìn)行確認(rèn);
情況二: 數(shù)據(jù)包就直接丟了
當(dāng)某一段報(bào)文段丟失之后, 發(fā)送端會(huì)一直收到 1001 這樣的ACK, 就像是在提醒發(fā)送端 "我想要的是 1001"一樣;
如果發(fā)送端主機(jī)連續(xù)三次收到了同樣一個(gè) "1001" 這樣的應(yīng)答, 就會(huì)將對(duì)應(yīng)的數(shù)據(jù) 1001 - 2000 重新發(fā)送;
這個(gè)時(shí)候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因?yàn)?001 - 7000)接收端其實(shí)之前就已經(jīng)收到了, 被放到了接收端操作系統(tǒng)內(nèi)核的接收緩沖區(qū)中;
這種機(jī)制被稱(chēng)為 "高速重發(fā)控制"(也叫 "快重傳").
3.3.6 流量控制
接收端處理數(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ā)送端.
接收端如何把窗口大小告訴發(fā)送端呢? 回憶我們的TCP首部中, 有一個(gè)16位窗口字段, 就是存放了窗口大小信息;
那么問(wèn)題來(lái)了, 16位數(shù)字最大表示65535, 那么TCP窗口最大就是65535字節(jié)么?
實(shí)際上, TCP首部40字節(jié)選項(xiàng)中還包含了一個(gè)窗口擴(kuò)大因子M, 實(shí)際窗口大小是? 窗口字段的值左移 M 位;
3.3.7 擁塞控制
雖然TCP有了滑動(dòng)窗口這個(gè)大殺器, 能夠高效可靠的發(fā)送大量的數(shù)據(jù). 但是如果在剛開(kāi)始階段就發(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īng)顟B(tài), 再?zèng)Q定按照多大的速度傳輸數(shù)據(jù);
此處引入一個(gè)概念程為擁塞窗口
發(fā)送開(kāi)始的時(shí)候, 定義擁塞窗口大小為1;
每次收到一個(gè)ACK應(yīng)答, 擁塞窗口加1;
每次發(fā)送數(shù)據(jù)包的時(shí)候, 將擁塞窗口和接收端主機(jī)反饋的窗口大小做比較, 取較小的值作為實(shí)際發(fā)送的窗口;
像上面這樣的擁塞窗口增長(zhǎng)速度, 是指數(shù)級(jí)別的. "慢啟動(dòng)" 只是指初使時(shí)慢, 但是增長(zhǎng)速度非???
為了不增長(zhǎng)的那么快, 因此不能使擁塞窗口單純的加倍.
此處引入一個(gè)叫做慢啟動(dòng)的閾值
當(dāng)擁塞窗口超過(guò)這個(gè)閾值的時(shí)候, 不再按照指數(shù)方式增長(zhǎng), 而是按照線性方式增長(zhǎng)
當(dāng)TCP開(kāi)始啟動(dòng)的時(shí)候, 慢啟動(dòng)閾值等于窗口最大值;
在每次超時(shí)重發(fā)的時(shí)候, 慢啟動(dòng)閾值會(huì)變成原來(lái)的一半, 同時(shí)擁塞窗口置回1;
少量的丟包, 我們僅僅是觸發(fā)超時(shí)重傳; 大量的丟包, 我們就認(rèn)為網(wǎng)絡(luò)擁塞;
當(dāng)TCP通信開(kāi)始后, 網(wǎng)絡(luò)吞吐量會(huì)逐漸上升; 隨著網(wǎng)絡(luò)發(fā)生擁堵, 吞吐量會(huì)立刻下降;
擁塞控制, 歸根結(jié)底是TCP協(xié)議想盡可能快的把數(shù)據(jù)傳輸給對(duì)方, 但是又要避免給網(wǎng)絡(luò)造成太大壓力的折中方案.
3.3.8 延遲應(yīng)答
如果接收數(shù)據(jù)的主機(jī)立刻返回ACK應(yīng)答, 這時(shí)候返回的窗口可能比較小.
假設(shè)接收端緩沖區(qū)為1M. 一次收到了500K的數(shù)據(jù); 如果立刻應(yīng)答, 返回的窗口就是500K;
但實(shí)際上可能處理端處理的速度很快, 10ms之內(nèi)就把500K數(shù)據(jù)從緩沖區(qū)消費(fèi)掉了;
在這種情況下, 接收端處理還遠(yuǎn)沒(méi)有達(dá)到自己的極限, 即使窗口再放大一些, 也能處理過(guò)來(lái);
如果接收端稍微等一會(huì)再應(yīng)答, 比如等待200ms再應(yīng)答, 那么這個(gè)時(shí)候返回的窗口大小就是1M;
一定要記得, 窗口越大, 網(wǎng)絡(luò)吞吐量就越大, 傳輸效率就越高. 我們的目標(biāo)是在保證網(wǎng)絡(luò)不擁塞的情況下盡量提高傳輸效率;
那么所有的包都可以延遲應(yīng)答么? 肯定也不是;
????????數(shù)量限制: 每隔N個(gè)包就應(yīng)答一次;
????????時(shí)間限制: 超過(guò)最大延遲時(shí)間就應(yīng)答一次;
具體的數(shù)量和超時(shí)時(shí)間, 依操作系統(tǒng)不同也有差異; 一般N取2, 超時(shí)時(shí)間取200ms;
3.3.9 捎帶應(yīng)答
在延遲應(yīng)答的基礎(chǔ)上, 我們發(fā)現(xiàn), 很多情況下, 客戶(hù)端服務(wù)器在應(yīng)用層也是 "一發(fā)一收" 的. 意味著客戶(hù)端給服務(wù)器說(shuō)了 "How are you", 服務(wù)器也會(huì)給客戶(hù)端回一個(gè) "Fine, thank you";
那么這個(gè)時(shí)候ACK就可以搭順風(fēng)車(chē), 和服務(wù)器回應(yīng)的 "Fine, thank you" 一起回給客戶(hù)端
3.4 面向字節(jié)流
創(chuàng)建一個(gè)TCP的socket, 同時(shí)在內(nèi)核中創(chuàng)建一個(gè)? 發(fā)送緩沖區(qū)? 和一個(gè)? 接收緩沖區(qū);
????????調(diào)用write時(shí), 數(shù)據(jù)會(huì)先寫(xiě)入發(fā)送緩沖區(qū)中;
????????如果發(fā)送的字節(jié)數(shù)太長(zhǎng), 會(huì)被拆分成多個(gè)TCP的數(shù)據(jù)包發(fā)出;
????????如果發(fā)送的字節(jié)數(shù)太短, 就會(huì)先在緩沖區(qū)里等待, 等到緩沖區(qū)長(zhǎng)度差不多了, 或者其他合適的時(shí)機(jī)發(fā)送出去;
????????接收數(shù)據(jù)的時(shí)候, 數(shù)據(jù)也是從網(wǎng)卡驅(qū)動(dòng)程序到達(dá)內(nèi)核的接收緩沖區(qū);
????????然后應(yīng)用程序可以調(diào)用read從接收緩沖區(qū)拿數(shù)據(jù);
????????另一方面, TCP的一個(gè)連接, 既有發(fā)送緩沖區(qū), 也有接收緩沖區(qū), 那么對(duì)于這一個(gè)連接, 既可以讀數(shù)據(jù), 也可以寫(xiě)數(shù)據(jù). 這個(gè)概念叫做? 全雙工
由于緩沖區(qū)的存在, TCP程序的讀和寫(xiě)不需要一一匹配, 例如:
????????寫(xiě)100個(gè)字節(jié)數(shù)據(jù)時(shí), 可以調(diào)用一次write寫(xiě)100個(gè)字節(jié), 也可以調(diào)用100次write, 每次寫(xiě)一個(gè)字節(jié);
????????讀100個(gè)字節(jié)數(shù)據(jù)時(shí), 也完全不需要考慮寫(xiě)的時(shí)候是怎么寫(xiě)的, 既可以一次read 100個(gè)字節(jié), 也可以一次read一個(gè)字節(jié), 重復(fù)100次;
3.4.1 粘包問(wèn)題
首先要明確, 粘包問(wèn)題中的 "包" , 是指的應(yīng)用層的數(shù)據(jù)包.
在TCP的協(xié)議頭中, 沒(méi)有如同UDP一樣的 "報(bào)文長(zhǎng)度" 這樣的字段, 但是有一個(gè)序號(hào)這樣的字段.
站在傳輸層的角度, TCP是一個(gè)一個(gè)報(bào)文過(guò)來(lái)的. 按照序號(hào)排好序放在緩沖區(qū)中.
站在應(yīng)用層的角度, 看到的只是一串連續(xù)的字節(jié)數(shù)據(jù).
那么應(yīng)用程序看到了這么一連串的字節(jié)數(shù)據(jù), 就不知道從哪個(gè)部分開(kāi)始到哪個(gè)部分, 是一個(gè)完整的應(yīng)用層數(shù)據(jù)包.
那么如何避免粘包問(wèn)題呢? 歸根結(jié)底就是一句話, 明確兩個(gè)包之間的邊界.
對(duì)于定長(zhǎng)的包, 保證每次都按固定大小讀取即可; 例如上面的Request結(jié)構(gòu), 是固定大小的, 那么就從緩沖區(qū)從頭開(kāi)始按sizeof(Request)依次讀取即可;
對(duì)于變長(zhǎng)的包, 可以在包頭的位置, 約定一個(gè)包總長(zhǎng)度的字段, 從而就知道了包的結(jié)束位置;
對(duì)于變長(zhǎng)的包, 還可以在包和包之間使用明確的分隔符(應(yīng)用層協(xié)議, 是程序猿自己來(lái)定的, 只要保證分隔符不和正文沖突即可);
4.5 TCP異常情況
進(jìn)程終止: 進(jìn)程終止會(huì)釋放文件描述符, 仍然可以發(fā)送FIN. 和正常關(guān)閉沒(méi)有什么區(qū)別.
機(jī)器重啟: 和進(jìn)程終止的情況相同.
機(jī)器掉電/網(wǎng)線斷開(kāi): 接收端認(rèn)為連接還在, 一旦接收端有寫(xiě)入操作, 接收端發(fā)現(xiàn)連接已經(jīng)不在了, 就
會(huì)進(jìn)行reset. 即使沒(méi)有寫(xiě)入操作, TCP自己也內(nèi)置了一個(gè)?;疃〞r(shí)器, 會(huì)定期詢(xún)問(wèn)對(duì)方是否還在. 如果對(duì)方不在, 也會(huì)把連接釋放.文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-712532.html
另外, 應(yīng)用層的某些協(xié)議, 也有一些這樣的檢測(cè)機(jī)制. 例如HTTP長(zhǎng)連接中, 也會(huì)定期檢測(cè)對(duì)方的狀態(tài). 例如QQ, 在QQ斷線之后, 也會(huì)定期嘗試重新連接.文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-712532.html
到了這里,關(guān)于計(jì)算機(jī)網(wǎng)絡(luò)中的應(yīng)用層和傳輸層(http/tcp)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!