深圳網(wǎng)站制作鄭州怎么優(yōu)化網(wǎng)站排名靠前
目錄:
再談“協(xié)議”
HTTP協(xié)議?
認識URL:
urlnecode和urldecode?
HTTP協(xié)議格式:
HTTP的方法:
簡易HTTP服務(wù)器:?
?傳輸層
再談端口號:?
端口號范圍劃分:
netstat:
pidof:
UDP協(xié)議
UDP協(xié)議端格式?:
檢驗和的解釋:
UDP的特點:
面向數(shù)據(jù)報:
UDP的緩沖區(qū):
UDP使用注意事項:
基于UDP的應(yīng)用層協(xié)議:
TCP協(xié)議?
TCP協(xié)議段格式:?
??編輯
超時重傳機制:?
連接管理機制:
理解TIME_WAIT狀態(tài):
滑動窗口:?
流量控制:
擁塞控制:
延遲應(yīng)答:?
捎帶應(yīng)答:
?面向字節(jié)流:
?粘包問題:
?TCP異常情況
TCP小結(jié):
基于TCP的應(yīng)用層協(xié)議:
TCP/UDP對比?
用UDP實現(xiàn)可靠傳輸(經(jīng)典面試題)
TCP的相關(guān)實驗?
理解listen的第二個參數(shù)??
Linux網(wǎng)絡(luò)編程套接字(上)https://blog.csdn.net/Obto_/article/details/132189802?
再談“協(xié)議”
協(xié)議是一種 "約定". socket api的接口, 在讀寫數(shù)據(jù)時, 都是按 "字符串" 的方式來發(fā)送接收的. 如果我們要傳輸一些"結(jié)構(gòu)化的數(shù)據(jù)" 怎么辦呢?
方案一:
- 客戶端發(fā)送一個"1+1"的字符串
- 這個字符串中會有兩個操作數(shù)且都是整形
- 兩個數(shù)據(jù)之間會有一個字符是運算符
- 數(shù)字和運算符之間沒有空格
- ....?
方案二:
- 定義結(jié)構(gòu)體來表示我們需要交互的信息
- 發(fā)送數(shù)據(jù)時將這個結(jié)構(gòu)體轉(zhuǎn)化成字符串,接收到的數(shù)據(jù)的時候再用相同的規(guī)則把字符串轉(zhuǎn)成結(jié)構(gòu)體
- 這個過程叫做“序列化”和“反序列化”
無論我們采用方案一還是方案二,亦或者其他的,其目的都是保證一端發(fā)送時夠早的數(shù)據(jù),在另一端能夠正確的進行解析,這種約定就是應(yīng)用層協(xié)議?
HTTP協(xié)議?
雖然說應(yīng)用層協(xié)議可以由我們程序員自己來定,但實際上,已經(jīng)有大佬定義了現(xiàn)成的,又非常好用,HTTP(超文本傳輸協(xié)議)就是其中之一?
認識URL:
平時我們俗稱的“網(wǎng)址”,其實就是URL
?
urlnecode和urldecode?
像 / ? : 等這樣的字符, 已經(jīng)被url當做特殊意義理解了. 因此這些字符不能隨意出現(xiàn).
比如, 某個參數(shù)中需要帶有這些特殊字符, 就必須先對特殊字符進行轉(zhuǎn)義.
轉(zhuǎn)義的規(guī)則如下:
將需要轉(zhuǎn)碼的字符轉(zhuǎn)為16進制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式
?
?比如我搜索:c++那么這兩個++就會被轉(zhuǎn)意成"%2B%2B"
?
HTTP協(xié)議格式:
//使用該指令可以在linux下查看url的請求
curl -I www.baidu.com
?
?
- 首行:[方法] + [url] + [版本]
- Header:請求的屬性,冒號分隔的禁止對,每組屬性之間用\n分隔,遇到空行表示Header結(jié)束?
- Body:空行后面的內(nèi)容都是Body(上圖沒有把Body截圖進去,太長了),Body允許為空,但如果Body存在,則在Header會有一個Content-Length屬性來表示Body的長度?
?
ps:Header中有的屬性不止圖上這些,這些只是較為常見的...?
HTTP的方法:
?
方法? ? ? ? ? ? ?? | 說明 | 支持的HTTP協(xié)議版本 |
---|---|---|
GET? ? ? ? ? ? ? ? | 獲取資源 | 1.0、1.1 |
POST | 傳輸實體主體 | 1.0、1.1 |
PUT | 傳輸文件 | 1.0、1.1 |
HEAD | 獲得報文首部 | 1.0、1.1 |
DELETE | 刪除文件 | 1.0、1.1 |
OPTIONS | 詢問支持的方法 | 1.1 |
TRACE | 追蹤路徑 | 1.1 |
CONNECT | 要求用隧道協(xié)議鏈接代理 | 1.1 |
LINK | 建立和資源之間的聯(lián)系 | 1.0 |
UNLINK | 斷開鏈接關(guān)系 | 1.0 |
HTTP的狀態(tài)碼?:
類別 | 原因 | |
---|---|---|
1XX | Informational(信息狀態(tài)碼) | 接受的請求正在處理 |
2XX | SUCCESS(成功狀態(tài)碼) | 請求正常處理完畢 |
3XX | Redirection(重定向狀態(tài)碼) | 需要進行附加操作以完成請求 |
4XX | Client Error(客戶端錯誤狀態(tài)碼) | 服務(wù)器無法處理請求 |
5XX | Server Error(服務(wù)器錯誤狀態(tài)碼) | 服務(wù)器處理請求出錯 |
常見的狀態(tài)碼:200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向)
504(Bad Gateway)?
HTTP常見的Header:
- ?Content-Type: 數(shù)據(jù)類型(text/html等)
- Content-Length: Body的長度
- Host: 客戶端告知服務(wù)器, 所請求的資源是在哪個主機的哪個端口上
- User-Agent: 聲明用戶的操作系統(tǒng)和瀏覽器版本信息
- referer: 當前頁面是從哪個頁面跳轉(zhuǎn)過來的
- location: 搭配3xx狀態(tài)碼使用, 告訴客戶端接下來要去哪里訪問
- Cookie: 用于在客戶端存儲少量信息. 通常用于實現(xiàn)會話(session)的功能
簡易HTTP服務(wù)器:?
HttpServer.hpp
#pragma once#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"class HttpServer
{
public:using func_t = std::function<void(int)>;private:int listensock_;uint16_t port_;Sock sock;func_t func_;public:HttpServer(const uint16_t &port, func_t func) : port_(port), func_(func){listensock_ = sock.Socket();sock.Bind(listensock_, port_);sock.Listen(listensock_);}void Start(){signal(SIGCHLD, SIG_IGN);for (;;){std::string clientIp;uint16_t clientPort = 0;int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);if (sockfd < 0)continue;if (fork() == 0){close(listensock_);func_(sockfd);close(sockfd);exit(0);}close(sockfd);}}~HttpServer(){if (listensock_ >= 0)close(listensock_);}
};
?HttpServer.cc
#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"// 一般http都要有自己的web根目錄
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客戶端只請求了一個/,我們返回默認首頁
#define HOMEPAGE "index.html"void HandlerHttpRequest(int sockfd)
{// 1. 讀取請求 for testchar buffer[10240];ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;// std::cout << buffer << "--------------------\n" << std::endl;}std::vector<std::string> vline;Util::cutString(buffer, "\n", &vline);std::vector<std::string> vblock;Util::cutString(vline[0], " ", &vblock);std::string file = vblock[1];std::string target = ROOT;if (file == "/")file = "/index.html";target += file;std::cout << target << std::endl;std::string content;std::ifstream in(target);if (in.is_open()){std::string line;while (std::getline(in, line)){content += line;}in.close();}std::string HttpResponse;if (content.empty())HttpResponse = "HTTP/1.1 404 NotFound\r\n";elseHttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "\r\n";HttpResponse += content;send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}void TestHandlerHttpRequest(int sockfd)
{std::string content = "<h1>ok1111</h1>";std::string HttpResponse;if (content.empty())HttpResponse = "HTTP/1.1 404 NotFound\r\n";elseHttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "Content-Length: 11\r\n";HttpResponse += "\r\n";HttpResponse += content;std::cout << "####start################" << std::endl;std::cout << HttpResponse << std::endl;//send(sockfd, content.c_str(), content.size(), 0);char buf[1024] = {0};// const char *hello = "<html><head><title>Hello, World!</title></head><body><h1>Hello, World!</h1><p>Welcome to my website.</p></body></html>";const char *hello = content.c_str();sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);//write(sockfd, buf, strlen(buf));write(sockfd, HttpResponse.c_str(), strlen(HttpResponse.c_str()));// send(sockfd,hello,sizeof(hello),0);std::cout << "#####end###############" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), TestHandlerHttpRequest));httpserver->Start();return 0;
}
?傳輸層
負責數(shù)據(jù)能夠從發(fā)送端傳輸接收端
再談端口號:?
端口號(Port)標識了一個主機上進行通信的不同應(yīng)用程序?
?在TCP/IP協(xié)議中, 用 "源IP", "源端口號", "目的IP", "目的端口號", "協(xié)議號" 這樣一個五元組來標識一個通信(可以通過netstat -n查看);
?
端口號范圍劃分:
-
0-1023:知名端口號,HTTP,FTP,SSH等這些廣為使用的應(yīng)用層協(xié)議,他們的端口號都是固定的
-
1024-65535:OS動態(tài)分配的端口號,客戶端程序的端口號,就是由OS從這個范圍分配的
有一些服務(wù)器是非常常用的,人們約定一些常用的服務(wù)器用的都是以下這些固定端口號
- ssh服務(wù)器:用22端口
- ftp服務(wù)器:用21端口
- telnet服務(wù)器:用23端口
- http服務(wù)器:用80端口
- https服務(wù)器用:用443端口?
執(zhí)行下面的命令, 可以看到知名端口號
cat /etc/services
netstat:
netstat使用來查看網(wǎng)絡(luò)狀態(tài)的工具:
語法:netstat [選項]
功能:查看網(wǎng)絡(luò)狀態(tài)
常用選項:
- n 拒絕顯示別名,能顯示數(shù)字的全部轉(zhuǎn)化成數(shù)字
- l 僅列出有在 Listen (監(jiān)聽) 的服務(wù)狀態(tài)
- p 顯示建立相關(guān)鏈接的程序名
- t (tcp)僅顯示tcp相關(guān)選項
- u (udp)僅顯示udp相關(guān)選項
- a (all)顯示所有選項,默認不顯示LISTEN相關(guān)
pidof:
查看服務(wù)器的進程id?
語法:pidof [進程名]
功能:通過進程名查找進程id?
UDP協(xié)議
UDP協(xié)議端格式?:
?
- 16UDP長度,表示整個數(shù)據(jù)報(UDP首部+UDP數(shù)據(jù))的最大長度
- 如果檢驗和出錯,就會直接丟棄?
檢驗和的解釋:
UDP的檢驗和可以幫助接收方驗證接收到的UDP數(shù)據(jù)是否完整、正確,并且未被損壞或篡改。發(fā)送方在發(fā)送UDP數(shù)據(jù)包時會計算數(shù)據(jù)包的檢驗和,并將該檢驗和值包含在UDP頭部中。接收方在接收數(shù)據(jù)包時,也會重新計算數(shù)據(jù)包的檢驗和,并將計算結(jié)果與接收到的檢驗和進行比較。如果兩個值不相等,就說明數(shù)據(jù)包在傳輸過程中發(fā)生了錯誤或篡改。?
UDP的特點:
UDP?傳輸?shù)倪^程類似寄信:
- 無連接:直到對端IP和PORT直接進行傳輸
- 不可靠:沒有確認機制,沒有重傳機制,如果網(wǎng)絡(luò)故障導致該數(shù)據(jù)段無法發(fā)送到對端,UDP協(xié)議也不會給應(yīng)用層返回任何的錯誤信息
- 面向數(shù)據(jù)包:不能夠靈活的控制讀寫數(shù)據(jù)的次數(shù)和數(shù)量(一次就發(fā)一個完整的報文)
面向數(shù)據(jù)報:
應(yīng)用層交給UDP多長的報文,UDP原樣發(fā)送,不會 拆分也不會合并
用UDP傳輸100個字節(jié)的數(shù)據(jù):
- ?如果發(fā)送端調(diào)用一次sendto, 發(fā)送100個字節(jié), 那么接收端也必須調(diào)用對應(yīng)的一次recvfrom, 接收100個字節(jié); 而不能循環(huán)調(diào)用10次recvfrom, 每次接收10個字節(jié)
UDP的緩沖區(qū):
UDP沒有真正意義上的發(fā)送緩沖區(qū),調(diào)用sendto會直接交給內(nèi)核,由該內(nèi)核數(shù)據(jù)傳給網(wǎng)絡(luò)協(xié)議進行后續(xù)的傳輸動作
UDP具有接收緩沖區(qū),但是這個接收緩沖區(qū)不能保證收到的UDP報的順序和發(fā)送UDP報的順序一致;如果緩沖區(qū)滿了,再到達的UDP數(shù)據(jù)會被丟棄
UDP使用注意事項:
我們注意到, UDP協(xié)議首部中有一個16位的最大長度. 也就是說一個UDP能傳輸?shù)臄?shù)據(jù)最大長度是64K(包含UDP首部).然而64K在當今的互聯(lián)網(wǎng)環(huán)境下, 是一個非常小的數(shù)字.如果我們需要傳輸?shù)臄?shù)據(jù)超過64K, 就需要在應(yīng)用層手動的分包, 多次發(fā)送, 并在接收端手動拼裝
基于UDP的應(yīng)用層協(xié)議:
-
NFS:網(wǎng)絡(luò)文件系統(tǒng)
-
TFTP:簡單文件傳輸協(xié)議
-
DHCP:動態(tài)主機配置協(xié)議
-
BOOTP:啟動協(xié)議
-
DNS:域名解析協(xié)議?
TCP協(xié)議?
TCP全稱為 "傳輸控制協(xié)議(Transmission Control Protocol"). 人如其名, 要對數(shù)據(jù)的傳輸進行一個詳細的控制
TCP協(xié)議段格式:?
?
- 源/目的端口號:表示數(shù)據(jù)是從哪個進程來,到哪個進程去
- 32位序號/32位確認號會在下文詳細講述
- 4位TCP報頭長度:表示該TCP頭部有多少個32位bit(有多少字節(jié));所以TCP頭部最大長度是15*4
- 6位標志位:
- URG:緊急指針是否有效
- ACK:確認號是否有效
- PSH:提示接收端應(yīng)用立刻從TCP緩沖區(qū)把數(shù)據(jù)讀走
- RST:對方要求重新建立連接;我們把攜帶RST表示的稱為復位報文段
- SYN:請求建立連接;我們把攜帶SYN標識的稱為同步報文段
- FIN:通知對方,本端要關(guān)閉了,我們稱攜帶FIN標識的為結(jié)束報文段
- 16位窗口大小:后面詳細講
- 16位校驗和:發(fā)送端填充,CRC校驗。接收端校驗不通過,則認為數(shù)據(jù)有問題。
- 16位緊急指針:表示那部分數(shù)據(jù)是緊急數(shù)據(jù)
?CRC校驗:
CRC校驗的原理如下:
- 首先,定義一個生成多項式(通常是二進制數(shù)),表示為G(X)(如0x8005)。
- 發(fā)送方計算數(shù)據(jù)的校驗碼,使用生成多項式G(X)進行計算。具體計算過程是將數(shù)據(jù)按照二進制形式做除法運算,除數(shù)為生成多項式G(X)。
- 將計算得到的校驗碼添加到數(shù)據(jù)后面,形成帶有校驗碼的數(shù)據(jù)包,然后發(fā)送給接收方。
- 接收方使用相同的生成多項式G(X)進行計算,將接收到的數(shù)據(jù)進行除法運算,得到一個余數(shù)。
- 如果接收方計算得到的余數(shù)為0,則說明數(shù)據(jù)在傳輸過程中沒有發(fā)生錯誤;如果余數(shù)不為0,則說明數(shù)據(jù)發(fā)生了錯誤或者被篡改。
?
?
TCP將每個字節(jié)的數(shù)據(jù)都進行編號,即序列號
?
?每一個ACK都帶有對應(yīng)的確認序列號,意思就是告訴發(fā)送者,我已經(jīng)收到了這個序列號之前的所有數(shù)據(jù),下一次你從這個序列號+1的后面開始發(fā)送
超時重傳機制:?
?
- 主機A發(fā)送數(shù)據(jù)給B之后,可能因為網(wǎng)絡(luò)擁堵等問題,數(shù)據(jù)無法到達B
- 如果主機A在一個特定時間間隔內(nèi)沒有收到B發(fā)來的確認應(yīng)答,就會重發(fā)?
當然還有下面這種情況:
?
因此主機B會收到很多重復數(shù)據(jù). 那么TCP協(xié)議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉.這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果
?
那么超時的時長如何定義:
- 最理想的情況下, 找到一個最小的時間, 保證 "確認應(yīng)答一定能在這個時間內(nèi)返回
- 但是這個時間的長短, 隨著網(wǎng)絡(luò)環(huán)境的不同, 是有差異的
- 如果超時時間設(shè)的太長, 會影響整體的重傳效率
- 如果超時時間設(shè)的太短, 有可能會頻繁發(fā)送重復的包
?TCP為了保證無論在任何環(huán)境下都能比較高性能的通信,因此會動態(tài)計算最大超時時間.
- ?Linux中(BSD Unix和Windows也是如此), 超時以500ms為一個單位進行控制, 每次判定超時重發(fā)的超時時間都是500ms的整數(shù)倍
- 如果重發(fā)一次之后, 仍然得不到應(yīng)答, 等待 2*500ms 后再進行重傳
- 如果仍然得不到應(yīng)答, 等待 4*500ms 進行重傳. 依次類推, 以指數(shù)形式遞增
- 累計到一定的重傳次數(shù), TCP認為網(wǎng)絡(luò)或者對端主機出現(xiàn)異常, 強制關(guān)閉連接
?
連接管理機制:
?
服務(wù)端的狀態(tài)轉(zhuǎn)換:?
- [CLOSED -> LISTEN]服務(wù)器端調(diào)用listen后進入LISTEN狀態(tài),等待客戶端連接
- [LISTEN->SYN_RCVD]一旦監(jiān)聽到連接請求(同步報文段),就將該連接放入內(nèi)核等待隊列中,并向客戶端發(fā)送SYN確認報文
- [SYN_RCVD -> ESTABLISHED]服務(wù)端一旦收到客戶端的確認報文,就進入ESTABLISHED狀態(tài),可以進行讀寫數(shù)據(jù)了
- [ESTABLISHED -> CLOSE_WAIT]當客戶端主動關(guān)閉(close()),服務(wù)器會收到結(jié)束報文段,服務(wù)器返回確認報文段并進入CLOSE_WAIT
- [CLOSE_WAIT -> LAST_ACK]進入CLOSE_WAIT后說明服務(wù)器準備關(guān)閉連接(需要處理完當前的數(shù)據(jù));當服務(wù)器真正調(diào)用close關(guān)閉連接時,會響客戶端發(fā)送FIN,此時服務(wù)器進入LAST_ACK狀態(tài),等待最后一個ACK到來(這個ACK是客戶端確認收到了FIN)
- [LAST_ACK -> CLOSED]服務(wù)端收到了對FIN的ACK,徹底關(guān)閉連接
客戶端的狀態(tài)轉(zhuǎn)換:??
- [CLOSED -> SYN_SENT] 客戶端調(diào)用connect, 發(fā)送同步報文段
- [SYN_SENT -> ESTABLISHED] connect調(diào)用成功, 則進入ESTABLISHED狀態(tài), 開始讀寫數(shù)據(jù)
- [ESTABLISHED -> FIN_WAIT_1] 客戶端主動調(diào)用close時, 向服務(wù)器發(fā)送結(jié)束報文段, 同時進入FIN_WAIT_1
- [FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務(wù)器對結(jié)束報文段的確認, 則進入FIN_WAIT_2, 開始等待服務(wù)器的結(jié)束報文段
- [FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務(wù)器發(fā)來的結(jié)束報文段, 進入TIME_WAIT, 并發(fā)出LAST_ACK
- [TIME_WAIT -> CLOSED] 客戶端要等待一個2MSL(Max Segment Life, 報文最大生存時間)的時間, 才會進入CLOSED狀態(tài)
?
理解TIME_WAIT狀態(tài):
可以做一個測試,首先啟動server,然后啟動client,再將Ctrl-C是server終止后再次運行server
就會綁定失敗:
$ ./server
bind error : Address already in use?
這是因為雖然server的應(yīng)用程序終止了,但是TCP協(xié)議層的連接并沒有完全斷開,因此不能再監(jiān)聽同樣的server端口?
- ?TCP協(xié)議規(guī)定,主動關(guān)閉連接的一方要處于TIME_ WAIT狀態(tài),等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態(tài)
- 我們使用Ctrl-C終止了server, 所以server是主動關(guān)閉連接的一方, 在TIME_WAIT期間仍然不能再次監(jiān)聽同樣的server端口
- MSL在RFC1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實現(xiàn)不同, 在Centos7上默認配置的值是60s
- 可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值
但是為什么是2MSL?
- ?MSL是TCP報文的最大生存時間, 因此TIME_WAIT持續(xù)存在2MSL的話
- 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經(jīng)消失
- 同時也是在理論上保證最后一個報文可靠到達(假設(shè)最后一個ACK丟失, 那么服務(wù)器會再重發(fā)一個FIN. 這時雖然客戶端的進程不在了, 但是TCP連接還在, 仍然可以重發(fā)LAST_ACK);
?
在server的TCP連接沒有完全斷開之前不允許監(jiān)聽,某些情況不太合理,下面是解決方法?
- ?使用setsockopt()設(shè)置socket描述符的 選項SO_REUSEADDR為1, 表示允許創(chuàng)建端口號相同但IP地址不同的多個socket描述符
int opt = 1;
setsockopt(listenfd , SOL_SOCKET , SO_REUSERADDR, &opt ,sizeof(opt));
滑動窗口:?
剛才我們討論了確認應(yīng)答策略, 對每一個發(fā)送的數(shù)據(jù)段, 都要給一個ACK確認應(yīng)答. 收到ACK后再發(fā)送下一個數(shù)據(jù)段.這樣做有一個比較大的缺點, 就是性能較差. 尤其是數(shù)據(jù)往返的時間較長的時候
?
但是這樣一收一發(fā)的效率很慢,(就像你去超市買菜,跑一趟就買一根,來回跑1000趟一樣)
但是如果我們一次性發(fā)送多條數(shù)據(jù)就可以大大提升效率(指的是你一次多帶點菜回來)?
?
- 窗口大小指的是無需等待確認應(yīng)答而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值. 上圖的窗口大小就是4000個字節(jié)(四個段)
- 發(fā)送前四個段的時候, 不需要等待任何ACK, 直接發(fā)送;
- 收到第一個ACK后, 滑動窗口向后移動, 繼續(xù)發(fā)送第五個段的數(shù)據(jù); 依次類推
- 操作系統(tǒng)內(nèi)核為了維護這個滑動窗口, 需要開辟 發(fā)送緩沖區(qū) 來記錄當前還有哪些數(shù)據(jù)沒有應(yīng)答; 只有確認應(yīng)答過的數(shù)據(jù), 才能從緩沖區(qū)刪掉
- 窗口越大, 則網(wǎng)絡(luò)的吞吐率就越高
?
?
那么再這種情況出現(xiàn)丟包,該如何重傳?
?情況一:數(shù)據(jù)包已經(jīng)到達,ACK丟失了
?
這種情況問題不大,因為可以通過后續(xù)的ACK來確認?
情況二:數(shù)據(jù)包直接就丟失了?
?
- 當某一段報文段丟失之后, 發(fā)送端會一直收到 1001 這樣的ACK, 就像是在提醒發(fā)送端 "我想要的是 1001"一樣
- 如果發(fā)送端主機連續(xù)三次收到了同樣一個 "1001" 這樣的應(yīng)答, 就會將對應(yīng)的數(shù)據(jù) 1001 - 2000 重新發(fā)送
- 這個時候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因為2001 - 7000)接收端其實之前就已經(jīng)收到了, 被放到了接收端操作系統(tǒng)內(nèi)核的接收緩沖區(qū)中
- 當出現(xiàn)三次重復的確認應(yīng)答就會進行重發(fā)
?
這種機制也被稱作"高速重發(fā)控制"(也稱“快重傳”)
流量控制:
?接收端處理數(shù)據(jù)的速度是有限的. 如果發(fā)送端發(fā)的太快, 導致接收端的緩沖區(qū)被打滿, 這個時候如果發(fā)送端繼續(xù)發(fā)送,就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應(yīng).因此TCP支持根據(jù)接收端的處理能力, 來決定發(fā)送端的發(fā)送速度. 這個機制就叫做流量控制(Flow Control)
- ?接收端將自己可以接收的緩沖區(qū)大小放入 TCP 首部中的 "窗口大小" 字段, 通過ACK端通知發(fā)送端
- 窗口大小字段越大, 說明網(wǎng)絡(luò)的吞吐量越高
- 接收端一旦發(fā)現(xiàn)自己的緩沖區(qū)快滿了, 就會將窗口大小設(shè)置成一個更小的值通知給發(fā)送端
- 發(fā)送端接受到這個窗口之后, 就會減慢自己的發(fā)送速度
- 如果接收端緩沖區(qū)滿了, 就會將窗口置為0; 這時發(fā)送方不再發(fā)送數(shù)據(jù), 但是需要定期發(fā)送一個窗口探測數(shù)據(jù)段, 使接收端把窗口大小告訴發(fā)送端
?
?
接收端如何把窗口大小告訴發(fā)送端呢? 回憶我們的TCP首部中, 有一個16位窗口字段, 就是存放了窗口大小信息;那么問題來了, 16位數(shù)字最大表示65535, 那么TCP窗口最大就是65535字節(jié)么?
實際上, TCP首部40字節(jié)選項中還包含了一個窗口擴大因子M, 實際窗口大小是 窗口字段的值左移 M 位
?
擁塞控制:
?雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠的發(fā)送大量的數(shù)據(jù). 但是如果在剛開始階段就發(fā)送大量的數(shù)據(jù), 仍然可能引發(fā)問題.因為網(wǎng)絡(luò)上有很多的計算機, 可能當前的網(wǎng)絡(luò)狀態(tài)就已經(jīng)比較擁堵. 在不清楚當前網(wǎng)絡(luò)狀態(tài)下, 貿(mào)然發(fā)送大量的數(shù)據(jù),是很有可能引起雪上加霜的
TCP引入慢啟動機制, 先發(fā)少量的數(shù)據(jù), 探探路, 摸清當前的網(wǎng)絡(luò)擁堵狀態(tài), 再決定按照多大的速度傳輸數(shù)據(jù)
- 此處引入一個概念程為擁塞窗口
- 發(fā)送開始的時候, 定義擁塞窗口大小為1
- 每次收到一個ACK應(yīng)答, 擁塞窗口加1
- 每次發(fā)送數(shù)據(jù)包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發(fā)送的窗口
像這樣的擁塞窗口增長速度是指數(shù)級別的
- 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍
- 此處引入一個叫做慢啟動的閾值
- 當擁塞窗口超過這個閾值的時候, 不再按照指數(shù)方式增長, 而是按照線性方式增長
- 當TCP開始啟動的時候, 慢啟動閾值等于窗口最大值
- 在每次超時重發(fā)的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回1
少量的丟包, 我們僅僅是觸發(fā)超時重傳; 大量的丟包, 我們就認為網(wǎng)絡(luò)擁塞;
當TCP通信開始后, 網(wǎng)絡(luò)吞吐量會逐漸上升; 隨著網(wǎng)絡(luò)發(fā)生擁堵, 吞吐量會立刻下降;
擁塞控制, 歸根結(jié)底是TCP協(xié)議想盡可能快的把數(shù)據(jù)傳輸給對方, 但是又要避免給網(wǎng)絡(luò)造成太大壓力的折中方案
?
延遲應(yīng)答:?
?如果接收數(shù)據(jù)的主機立刻返回ACK應(yīng)答, 這時候返回的窗口可能比較小
- ?假設(shè)接收端緩沖區(qū)為1M. 一次收到了500K的數(shù)據(jù); 如果立刻應(yīng)答, 返回的窗口就是500K
- 但實際上可能處理端處理的速度很快, 10ms之內(nèi)就把500K數(shù)據(jù)從緩沖區(qū)消費掉了
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來
- 如果接收端稍微等一會再應(yīng)答, 比如等待200ms再應(yīng)答, 那么這個時候返回的窗口大小就是1M
?
窗口越大,網(wǎng)絡(luò)吞吐量就越大,傳輸效率就越高,我們的目標是保證網(wǎng)絡(luò)不擁塞的情況下,盡量提高傳輸效率
那么所有的包都可以延遲應(yīng)答么? 肯定也不是
- 數(shù)量限制: 每隔N個包就應(yīng)答一次(一般N=2,超時時間取200ms)
- 時間限制: 超過最大延遲時間就應(yīng)答一次
?
捎帶應(yīng)答:
在延遲應(yīng)答的基礎(chǔ)上, 我們發(fā)現(xiàn), 很多情況下, 客戶端服務(wù)器在應(yīng)用層也是 "一發(fā)一收" 的. 意味著客戶端給服務(wù)器說了"How are you", 服務(wù)器也會給客戶端回一個 "Fine, thank you";那么這個時候ACK就可以搭順風車, 和服務(wù)器回應(yīng)的 "Fine, thank you" 一起回給客戶端
?
?面向字節(jié)流:
?創(chuàng)建一個TCP的socket, 同時在內(nèi)核中創(chuàng)建一個 發(fā)送緩沖區(qū) 和一個 接收緩沖區(qū);
- ?調(diào)用write時, 數(shù)據(jù)會先寫入發(fā)送緩沖區(qū)中
- 如果發(fā)送的字節(jié)數(shù)太長, 會被拆分成多個TCP的數(shù)據(jù)包發(fā)出
- 如果發(fā)送的字節(jié)數(shù)太短, 就會先在緩沖區(qū)里等待, 等到緩沖區(qū)長度差不多了, 或者其他合適的時機發(fā)送出去
- 接收數(shù)據(jù)的時候, 數(shù)據(jù)也是從網(wǎng)卡驅(qū)動程序到達內(nèi)核的接收緩沖區(qū)
- 然后應(yīng)用程序可以調(diào)用read從接收緩沖區(qū)拿數(shù)據(jù)
- 另一方面, TCP的一個連接, 既有發(fā)送緩沖區(qū), 也有接收緩沖區(qū), 那么對于這一個連接, 既可以讀數(shù)據(jù), 也可以寫數(shù)據(jù). 這個概念叫做 全雙工
?由于緩沖區(qū)的存在, TCP程序的讀和寫不需要一一匹配(不同于UDP), 例如:
- ?寫100個字節(jié)數(shù)據(jù)時, 可以調(diào)用一次write寫100個字節(jié), 也可以調(diào)用100次write, 每次寫一個字節(jié)
- 讀100個字節(jié)數(shù)據(jù)時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100個字節(jié), 也可以一次read一個字節(jié), 重復100次;
?粘包問題:
- 首先要明確, 粘包問題中的 "包" , 是指的應(yīng)用層的數(shù)據(jù)包
- 在TCP的協(xié)議頭中, 沒有如同UDP一樣的 "報文長度" 這樣的字段, 但是有一個序號這樣的字段
- 站在傳輸層的角度, TCP是一個一個報文過來的. 按照序號排好序放在緩沖區(qū)中
- 站在應(yīng)用層的角度, 看到的只是一串連續(xù)的字節(jié)數(shù)據(jù)
- 那么應(yīng)用程序看到了這么一連串的字節(jié)數(shù)據(jù), 就不知道從哪個部分開始到哪個部分, 是一個完整的應(yīng)用層數(shù)據(jù)包
那么如何避免粘包問題呢? 歸根結(jié)底就是一句話, 明確兩個包之間的邊界
- 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的Request結(jié)構(gòu), 是固定大小的, 那么就從緩沖區(qū)從頭開始按sizeof(Request)依次讀取即可
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結(jié)束位置
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應(yīng)用層協(xié)議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可)
?
?對于UDP來說,不存在粘包問題:
- 對于UDP, 如果還沒有上層交付數(shù)據(jù), UDP的報文長度仍然在. 同時, UDP是一個一個把數(shù)據(jù)交付給應(yīng)用層. 就有很明確的數(shù)據(jù)邊界
- 站在應(yīng)用層的站在應(yīng)用層的角度, 使用UDP的時候, 要么收到完整的UDP報文, 要么不收. 不會出現(xiàn)"半個"的情況
?
?TCP異常情況
進程終止: 進程終止會釋放文件描述符, 仍然可以發(fā)送FIN. 和正常關(guān)閉沒有什么區(qū)別
機器重啟: 和進程終止的情況相同
機器掉電/網(wǎng)線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發(fā)現(xiàn)連接已經(jīng)不在了, 就會進行reset. 即使沒有寫入操作, TCP自己也內(nèi)置了一個?;疃〞r器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放
另外, 應(yīng)用層的某些協(xié)議, 也有一些這樣的檢測機制. 例如HTTP長連接中, 也會定期檢測對方的狀態(tài). 例如QQ, 在QQ
斷線之后, 也會定期嘗試重新連接
?
TCP小結(jié):
?為什么TCP這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能
可靠性:
- 檢驗和
- 序列號
- 確認應(yīng)答
- 超時重發(fā)
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應(yīng)答
- 捎帶應(yīng)答
其他:?
- 定時器(超時重傳定時器,?;疃〞r器,TIME_WAIT定時器等)
基于TCP的應(yīng)用層協(xié)議:
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
- 還有你自己寫TCP程序自定義的應(yīng)用層協(xié)議?
TCP/UDP對比?
- TCP用于可靠傳輸?shù)那闆r, 應(yīng)用于文件傳輸, 重要狀態(tài)更新等場景
- UDP用于對高速傳輸和實時性要求較高的通信領(lǐng)域, 例如, 早期的QQ, 視頻傳輸?shù)? 另外UDP可以用于廣播
用UDP實現(xiàn)可靠傳輸(經(jīng)典面試題)
參考tcp的可靠性機制
- 引入序列號,保證數(shù)據(jù)順序
- 引入確認應(yīng)答,確保對端收到了數(shù)據(jù)
- 引出超時重傳,如果隔一段時間沒有應(yīng)答,就重發(fā)數(shù)據(jù)
TCP的相關(guān)實驗?
理解listen的第二個參數(shù)?
這里將listen的第二個參數(shù)改成2,并且不調(diào)用accept
test.server.cc
#include "tcp_socket.hpp"
int main(int argc, char *argv[])
{if (argc != 3){printf("Usage ./test_server [ip] [port]\n");return 1;}TcpSocket sock;bool ret = sock.Bind(argv[1], atoi(argv[2]));if (!ret){return 1;}ret = sock.Listen(2);if (!ret){return 1;}// 客戶端不進行 acceptwhile (1){sleep(1);}return 0;
}
test.client.cc
#include "tcp_socket.hpp"
int main(int argc, char *argv[])
{if (argc != 3){printf("Usage ./test_client [ip] [port]\n");return 1;}TcpSocket sock;bool ret = sock.Connect(argv[1], atoi(argv[2]));if (ret){printf("connect ok\n");}else{printf("connect failed\n");}while (1){sleep(1);}return 0;
}
此時啟動 3 個客戶端同時連接服務(wù)器, 用 netstat 查看服務(wù)器狀態(tài), 一切正常.
但是啟動第四個客戶端時, 發(fā)現(xiàn)服務(wù)器對于第四個連接的狀態(tài)存在問題了
客戶端狀態(tài)正常, 但是服務(wù)器端出現(xiàn)了 SYN_RECV 狀態(tài), 而不是 ESTABLISHED 狀態(tài)
這是因為, Linux內(nèi)核協(xié)議棧為一個tcp連接管理使用兩個隊列:
- 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態(tài)的請求)
- 全連接隊列(accpetd隊列)(用來保存處于established狀態(tài),但是應(yīng)用層沒有調(diào)用accept取走的請求)
而全連接隊列的長度會受到 listen 第二個參數(shù)的影響,全連接隊列滿了的時候, 就無法繼續(xù)讓當前連接的狀態(tài)進入 established 狀態(tài),這個隊列的長度通過上述實驗可知, 是 listen 的第二個參數(shù) + 1
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
??
?
?
?
?
?
?
?
?
?
?
?