目錄
- 1. 序列化和反序列化
- 1.1 序列化
- 1.2 反序列化
- 2. 網絡版本計算器(自定義協議)
- 3. 再次理解OSI七層模型
- 4. HTTP協議
- 4.1 HTTP協議格式
- 4.2 HTTP的方法
- 4.3 HTTP的狀態碼
- 4.4 HTTP常見Header
- 4.5 長連接和短連接
- 4.6 Cookie
- 5. HTTPS協議
- 5.1 對稱加密和非對稱加密概念
- 5.2 HTTPS:對稱加密+非對稱加密+證書認證
- 6. UDP協議
- 6.1 UDP協議的特點
- 6.2 UDP協議端格式
- 6.3 UDP有接收緩沖區,沒有發送緩沖區。
- 7. TCP協議
- 7.1 TCP協議段格式
- 7.1.1 為什么一個TCP報文中同時需要序號和確認序號?
- 7.1.2 TCP為什么是面向字節流的,UDP是面向數據報呢?
- 7.2 確認應答(ACK)機制
- 7.3 超時重傳機制
- 7.4 TCP三次握手
- 7.4.1 連接管理機制
- 7.4.2 為什么握手是三次
- 7.5 TCP 四次揮手
- 7.6 滑動窗口
- 7.6.1 滑動窗口的結構
- 7.6.2 快重傳
- 7.7 延遲應答
- 7.7.1 延遲應答的概念
- 7.7.2 延遲應答的作用和觸發條件
- 7.8 流量控制
- 7.9 擁塞控制
- 7.10 粘包問題
- 7.10 TCP的異常退出
1. 序列化和反序列化
序列化:value 對象 ——> str 字符串
1.1 序列化
函數原型:
作用:把 Json::Value 對象轉化為 格式化的 JSON 字符串(有縮進、有換行)
namespace Json {class StyledWriter {public:std::string write(const Json::Value& root);};
}作用:把 Json::Value 對象轉化為 緊湊的 JSON 字符串(無縮進、無換行)
namespace Json {class FastWriter {public:std::string write(const Json::Value& root);};
}
示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>int main()
{int x=1, y=2; char op='/';Json::Value root;std::string str1;std::string str2;root["x"] = x;root["y"] = y;root["op"] = op;Json::FastWriter w1; //JSON緊湊寫入器(無縮進、無換行)Json::StyledWriter w2; // JSON美化寫入器(有縮進、有換行)str1 = w1.write(root); // 序列化str2 = w2.write(root); // 序列化std::cout << str1 << std::endl;std::cout<<std::endl;std::cout << str2 << std::endl;return 0;
}
輸出:
{"x":1,"y":2,"op":"/"}{"x" : 1,"y" : 2,"op" : "/"
}
1.2 反序列化
反序列化:str 字符串 ——> value 對象
函數原型:
namespace Json {class Reader {public:bool parse(const std::string& document, Value& root, bool collectComments = true);};
}
示例:
std::string str="{"x" : 1,"y" : 2,"op" : "/"
}";
Json::Value root;
Json::Reader r;
r.parse(str, root); // 反序列化
2. 網絡版本計算器(自定義協議)
源碼如下:網絡版本計算器
在博客里面就放了一個套接字封裝和自定義協議,想看完整代碼可以查看gitee源碼。
Socket.hpp: 套接字封裝
#pragma once
#include<iostream>
#include<sys/socket.h> //套接字函數(如:socket()...)
#include<netinet/in.h> //定義網絡地址結構(如 struct sockaddr_in)和協議常量(如 AF_INET、SOCK_STREAM)
#include<arpa/inet.h> //IP 地址轉換函數,字節序轉換函數
#include<unistd.h>
#include<cstring>
#include<string>
#include<assert.h>
#include"Log.hpp" //TCP服務端:1. socket() → 2. bind() → 3. listen() → 4. accept() → 5. write()/read() → 6. close()
//TCP客戶端:1. socket() → 2. connect() → 3. write()/read() → 4. close()//客戶端比較于服務端來說,只有3個不同:
//1. 客戶端不用自己綁定端口,系統自己綁定,即沒有bind函數
//2. 服務端在連接之前還要listen
//3. 客戶端:connect 服務端:accept//socket:直接得到兩端的套接字
//connect:要服務端的所有信息 accept:得到客戶端的所有信息 只有accept能得到新套接字(連接套接字)
//注意:write()/read()只需要一個套接字就行,而客戶端還是用socket的那個套接字,而服務端必須用新套接字//主機序列,網絡字節序的轉化永遠是int在轉(指uint32_t/uint64_t),所以只有port才會轉,才要轉//使用說明:
//Sock listensock;
//listensock.Sock(); ... enum
{Socket_ERR=1,Bind_ERR,Listen_ERR,Accept_ERR,Close_ERR,Connect_ERR,
};//適用于TCP的客戶端和服務端
class Sock
{private:int _socket;public:void Socket(){_socket=socket(AF_INET, SOCK_STREAM, 0);if(_socket<0){lg(Fatal,"socket error, _socket: %d。 %s",_socket,strerror(errno));exit(Socket_ERR);}}void Bind(uint16_t port){struct sockaddr_in address;address.sin_addr.s_addr=INADDR_ANY; address.sin_family=AF_INET;address.sin_port=htons(port);if(bind(_socket,(struct sockaddr*)&address,sizeof(address))<0){lg(Fatal,"bind error, _socket: %d。 %s",_socket,strerror(errno));exit(Bind_ERR);}}//服務端void Listen(){if(listen(_socket,10)){lg(Fatal,"listen error, _socket: %d。 %s",_socket,strerror(errno));exit(Listen_ERR);}}//服務端專業函數,返回連接套接字//可以得到客戶端的信息,雖然這些信息不能對讀寫有任何幫助,然后需要打印出客戶信息int Accept(std::string* clientip,uint16_t* clientport){ struct sockaddr_in tmp;memset(&tmp,0,sizeof(tmp));socklen_t len=sizeof(tmp);int newfd=accept(_socket,(sockaddr*)&tmp,&len);if(newfd<0){lg(Fatal,"accept error, _socket: %d; newfd: %d 。%s",_socket,newfd,strerror(errno));exit(Accept_ERR);}char ipstr[64];*clientport=ntohs(tmp.sin_port);inet_ntop(AF_INET, &tmp, ipstr,sizeof(ipstr));*clientip=ipstr;return newfd;}//客戶端//需要服務端的信息bool Connect(const std::string &serverip,const uint16_t &serverport){sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&(server.sin_addr));socklen_t len=sizeof(server);if(connect(_socket,(sockaddr*)&server,len)<0){std::cerr << "connect to " << serverip << ":" << serverport << " error" << std::endl;return false; }return true;}void Close(){if(close(_socket)<0){exit(Close_ERR);}}int Fd(){return _socket;}
};
Protocol.hpp: 自定義協議
#pragma once
#include<string>
#include"json/json.h"//序列化,反序列化,編碼,解碼//使用說明:
//Request:
//Request req(data1,data2,op); req.Serialize(&str); //序列化, 通過data1,data2,op 得到了 str
//Request req; req.DeSerialize(str); //反序列化, 通過str 得到了 data1,data2,op//Response:
//Response rsp(result,code); rsp.Serialize(&str); //序列化, 通過result,code 得到了 str
//Response rsp; rsp.DeSerialize(str); //反序列化, 通過str 得到了 result,codeconst std::string protocol_sep = "\n";//編碼: 發送的報文是:"內容長度" + 分隔符 + "原始內容" + 分隔符
void EnCode(const std::string& content,std::string *package)
{*package+=std::to_string(content.size());*package+=protocol_sep;*package+=content;*package+=protocol_sep;
}//解碼
bool DeCode(std::string& package,std::string *content) //package可能會有兩條內容或者半條內容
{//找分隔符auto pos=package.find(protocol_sep);if(pos==std::string::npos)return false;//提取內容長度std::string len_str =package.substr(0,pos);int len=std::stoi(len_str.c_str()); //內容長度//查看整個報文是否有一條報文的長度int total_len=len_str.size()+2+len;if(package.size()<total_len)return false;//提取len長度的內容*content=package.substr(pos+1,len);//package丟棄一條報文長度package.erase(0,total_len);return true;
}class Request
{public:int _data1;int _data2;char _op; //value不能有char類型,value中直接用int就行public:Request(int data1,int data2,char op):_data1(data1),_data2(data2),_op(op){}Request(){}// 用root(用類成員生成) 轉成strbool Serialize(std::string* str){Json::Value root;root["data1"]=_data1;root["op"]=(int)_op;root["data2"]=_data2;Json::StyledWriter w;*str=w.write(root);return true;}// 已知的str 轉成root(root可以完善類)bool DeSerialize(const std::string str){Json::Value root;Json::Reader r; r.parse(str,root);_data1=root["data1"].asInt();_data2=root["data2"].asInt();_op=root["op"].asInt();return true;}std::string GetRequest(){std::string quest;quest+=std::to_string(_data1);quest+=_op;quest+=std::to_string(_data2);quest+="=?";return quest;}};class Response
{public:int _result=0;int _code=0; // 0,可信,否則!0具體是幾,表明對應的錯誤原因public:// 用root(用類成員生成) 轉成strbool Serialize(std::string* str){Json::Value root;root["result"]=_result;root["code"]=_code;Json::StyledWriter w;*str=w.write(root);return true;}// 已知的str 轉成root(root可以完善類)bool DeSerialize(const std::string str){Json::Value root;Json::Reader r; r.parse(str,root);_result=root["result"].asInt();_code=root["code"].asInt();return true;}std::string GetResult(){std::string ret;ret+="result: ";ret+=std::to_string(_result);ret+=" code: ";ret+=std::to_string(_code);return ret;}
};
3. 再次理解OSI七層模型
4. HTTP協議
4.1 HTTP協議格式
4.2 HTTP的方法
HTTP的方法最主要的就是GET 和 POST。
GET:用于請求數據(從服務器獲取資源,如網頁、圖片、API數據)。
POST:用于提交數據(向服務器發送數據以創建或修改資源,如表單提交、文件上傳)。
GET:
- 數據通過URL參數傳遞(附加在URL后,形如 ?key1=value1&key2=value2)。
- 數據可見(暴露在地址欄、瀏覽器歷史、服務器日志中)。
- 有長度限制(受瀏覽器和服務器限制,通常不超過2048字符)。
POST
- 數據通過請求體(Request Body)傳遞。
- 數據不可見(不顯示在URL中,適合敏感信息)。
- 無嚴格長度限制(可傳輸大量數據,如文件上傳)
4.3 HTTP的狀態碼
最常見的狀態碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
4.4 HTTP常見Header
請求頭(Request Headers)
客戶端發送給服務器的頭信息,用于告知服務器客戶端的請求信息:
- Host
告訴服務器請求的資源所在的主機和端口(主機和端口是服務端的)(如 Host: www.example.com:443)。 - User-Agent
聲明客戶端的操作系統、瀏覽器版本等信息(如 User-Agent: Mozilla/5.0 (Windows NT 10.0))。 - Referer
表示當前請求是從哪個頁面跳轉過來的(如 Referer: https://www.google.com)。 - Cookie
客戶端攜帶的Cookie數據,用于會話管理(如 Cookie: session_id=abc123)。
響應頭(Response Headers)
服務器返回給客戶端的頭信息,用于控制客戶端行為或補充響應內容:
- Content-Type
告知客戶端返回數據的類型(如 Content-Type: text/html; charset=utf-8)。 - Content-Length
表示響應體的長度(字節數),如 Content-Length: 1024。 - Location
搭配 3xx重定向狀態碼,告訴客戶端下一步跳轉的URL(如 Location: /new-page)。
特殊情況說明
- Cookie 雖然是客戶端發送的,但服務器也可以通過 Set-Cookie(響應頭)讓客戶端存儲Cookie。
- Content-Type 和 Content-Length 在極少數情況下也可能出現在請求頭中(如POST請求提交數據時)。
4.5 長連接和短連接
短連接
特點:
1.每次請求-響應后關閉 TCP 連接。
2. 下次請求需重新建立連接(三次握手)。
3. HTTP/1.0 默認行為(除非顯式設置 Connection: keep-alive)。
工作流程:
1.客戶端發起請求 → TCP 三次握手建立連接。
2. 服務器返回響應。
3. 服務器主動關閉 TCP 連接(四次揮手)。
4. 后續請求重復步驟 1~3。
缺點
1.高延遲:每次請求需重新握手,增加 RTT(Round-Trip Time)時間。
2.資源浪費:頻繁創建/銷毀連接消耗 CPU 和內存。
3.性能瓶頸:不適合高頻請求場景(如網頁加載多個資源)。
適用場景
1.低頻請求(如傳統靜態網頁)。
2.無需保持狀態的簡單交互。
長連接
特點
1.復用 TCP 連接處理多個請求-響應。
2.默認在 HTTP/1.1 中啟用(無需顯式設置)。
3.通過 Connection: keep-alive 頭部協商(HTTP/1.0 需手動開啟)。
工作流程
1.客戶端發起首次請求 → TCP 三次握手建立連接。
2.服務器返回響應,保持連接不關閉。
3.客戶端復用同一 TCP 連接發送后續請求。
4.空閑一段時間后(超時時間由服務器設定),連接自動關閉。
優點
1.降低延遲:避免重復握手(尤其 HTTPS 的 TLS 握手更耗時)。
2.減少資源消耗:復用連接減少 CPU/內存開銷。
3.提升吞吐量:適合高頻請求(如現代網頁加載 JS/CSS/圖片)。
適用場景
1.高頻請求(如 API 調用、動態網頁)。
2.需要低延遲的交互(如 WebSocket 前置握手)。
Connection (請求頭和響應頭)
Connection 是一個 HTTP 請求頭(Request Header)和響應頭(Response Header),用于控制當前 TCP 連接的行為,尤其是決定是否保持長連接(Keep-Alive)。
- 客戶端請求頭:
客戶端通過 Connection 頭告知服務器是否希望保持長連接。
GET /example HTTP/1.1
Host: api.example.com
Connection: keep-alive # 表示客戶端希望保持連接
- 服務器響應頭:
服務器通過 Connection 頭確認是否支持長連接。
HTTP/1.1 200 OK
Connection: keep-alive # 服務器同意保持連接
Keep-Alive: timeout=60, max=1000
Connection 可以取值 keep-alive 和 close :
keep-alive:客戶端或服務器希望保持連接(HTTP/1.1 默認啟用,無需顯式設置)。
close:明確要求當前請求完成后關閉連接(使用短連接)。
4.6 Cookie
Cookie 是 HTTP 協議中用于 在客戶端(瀏覽器)存儲小型數據 的機制,主要用于會話管理(如用戶登錄狀態)、個性化設置(如語言偏好)和用戶行為跟蹤(如廣告定向)。以下是全面解析:
-
Cookie 的工作原理
基本流程:- 服務器設置 Cookie
通過 HTTP 響應頭的 Set-Cookie 字段向瀏覽器發送 Cookie:
HTTP/1.1 200 OK Set-Cookie: session_id=abc123; Path=/; Secure; HttpOnly
- 瀏覽器存儲 Cookie
瀏覽器將 Cookie 存儲在本地(內存或硬盤),后續請求自動附加到請求頭的 Cookie 字段:
GET /profile HTTP/1.1 Cookie: session_id=abc123
- 服務器讀取 Cookie
服務器解析請求頭的 Cookie 字段,識別用戶身份或狀態。
- 服務器設置 Cookie
-
Cookie 的分類
-
會話 Cookie(Session Cookie)
不設置 Expires 或 Max-Age,瀏覽器關閉后自動刪除。 -
持久 Cookie(Persistent Cookie)
設置過期時間,長期存儲在硬盤中。
-
-
Cookie 的應用場景
(1)會話管理
用戶登錄后,服務器下發 Session ID(Session ID是根據用戶名和密碼生成的在整個服務器中的唯一的ID,確保每個用戶都不一樣)Set-Cookie: session_id=xyz789; Path=/; HttpOnly; Secure; SameSite=Lax
后續請求自動攜帶該 Cookie,服務器驗證 Session ID 維持登錄狀態。
(2)個性化設置
存儲用戶語言、主題偏好
5. HTTPS協議
HTTPS = HTTP + TLS/SSL 加密
TLS/SSL在應用層:
5.1 對稱加密和非對稱加密概念
對稱加密:
定義:對稱加密是指加密和解密使用相同密鑰的加密方式。
特點:
- 加解密速度快,適合大數據量加密
- 密鑰管理困難(需要安全地共享密鑰)
- 算法相對簡單
工作流程:
- 通信雙方協商一個共享密鑰
- 發送方用該密鑰加密數據
- 接收方用相同密鑰解密數據
典型應用場景:
- 大量數據的加密(如文件加密、數據庫加密)
- SSL/TLS協議中的數據加密部分
- 磁盤加密
非對稱加密:
定義:非對稱加密使用一對密鑰(公鑰和私鑰),公鑰用于加密,私鑰用于解密
。
特點:
- 加解密速度慢,不適合大數據量加密
- 解決了密鑰分發問題
- 可實現數字簽名功能
- 算法復雜度高
工作流程:
- 接收方生成密鑰對(公鑰和私鑰)
- 接收方將公鑰發送給發送方
- 發送方用公鑰加密數據
- 接收方用私鑰解密數據
典型應用場景:
- 安全密鑰交換(如SSL/TLS握手)
- 數字簽名
- 身份驗證
- 小數據量加密
5.2 HTTPS:對稱加密+非對稱加密+證書認證
HTTPS使用對稱加密+非對稱加密+證書認證的方式來加密:
-
如果只使用對稱加密+非對稱加密來加密:
關鍵問題就是:服務端發來的公鑰被調包了,即客戶端沒法判斷公鑰是否是合法的。
我們可以用證書認證來證明公鑰的合法性。 -
證書(使用了數字簽名):
簽名形成的過程也被叫做對數據進行數字簽名。
數字簽名是基于非對稱加密算法。 -
如何使用對稱加密+非對稱加密+證書認證來數據傳輸:
客戶端瀏覽器都內置了CA機構的公鑰
-
步驟1:驗證證書的真假:
- 客戶端用CA公鑰對發來的證書的簽名進行解密
- 解密后的結果和INFO進行對比,
相等,這個證書就是CA機構驗證過的證書;不相等,就是假證書 - 查看INFO中的域名是否和服務端一樣,一樣就是服務端的證書
防止中間人在CA機構申請了證書來竊聽消息。(域名具有唯一性)
-
步驟2:提取證書中的公鑰a,客戶端用公鑰a來加密密鑰b傳輸給服務端
-
步驟3:雙方用密鑰b來傳輸消息。
-
-
提示:
- CA機構的公鑰用于解密,私鑰用于加密,這是一個特例。
- 如果中間人修改了證書的INFO,那么客戶端用公鑰解密后,簽名和INFO不匹配;如果中間人修改了證書的簽名,他沒有私鑰加密,那么最后客戶端用公鑰解密后,簽名和INFO還不匹配。
- 只有真正的CA機構的證書才會簽名和INFO匹配。即使中間人使用了真正的CA證書,客戶端查看證書INFO域名也會知道這不是客戶端域名
總結
HTTPS ?作過程中涉及到的密鑰有三組:
第?組(?對稱加密): ?于校驗證書是否被篡改。
第?組(?對稱加密): ?于傳遞對稱加密的密鑰.
第三組(對稱加密): 客?端和服務器后續傳輸的數據都通過這個對稱密鑰加密解密.
6. UDP協議
UDP(User Datagram Protocol,用戶數據報協議)是傳輸層的協議,位于OSI模型的第四層和TCP/IP模型的傳輸層。
6.1 UDP協議的特點
- 無連接: 知道對端的IP和端口號就直接進行傳輸, 不需要建立連接;
- 不可靠: 沒有確認機制, 沒有重傳機制; 如果因為網絡故障該段無法發到對方, UDP協議層也不會給應用層返回任何錯誤信息;
- 面向數據報: 不能夠靈活的控制讀寫數據的次數和數量
6.2 UDP協議端格式
6.3 UDP有接收緩沖區,沒有發送緩沖區。
接收緩沖區在傳輸層。
- UDP 的發送是“直接提交”:調用 sendto() 時,數據通常直接交給網絡層(IP 層),不會像 TCP 一樣在傳輸層緩存。
- UDP 通常沒有發送緩沖區(數據直接提交給網絡層)。這是因為UDP 的輕量化設計:犧牲緩沖和可靠性,換取更低的開銷和延遲(適合實時應用如視頻、DNS)。UDP 的無連接和不可靠特性決定了其緩沖機制的簡化。
- UDP 有接收緩沖區(防止數據丟失,但滿時會丟棄新數據)。
- TCP 的復雜性:需要緩沖區管理重傳、排序、流量控制等機制。
- UDP是全雙工的。全雙工:允許同一套接字(Socket)同時發送和接收數據
7. TCP協議
TCP協議也是在傳輸層。TCP全稱為 “傳輸控制協議”。
7.1 TCP協議段格式
-
32位序號
- 作用:
- 標識 當前報文段數據部分的第一個字節 在整個數據流中的位置(字節偏移量)。
- 用于解決亂序問題,確保接收方能按正確順序重組數據。
- 初始值(ISN):
- 在 TCP 三次握手時,雙方隨機生成初始序號(ISN),避免歷史報文干擾。
- 增長規則:
- 每次發送數據后,序號按 數據字節數 遞增。例如:
發送 100 字節數據(序號=0),則下一個報文段的序號=100。
- 每次發送數據后,序號按 數據字節數 遞增。例如:
- 作用:
-
32位確認序號
- 作用:
- 期望收到的下一個字節的序號,表示該序號之前的所有數據已正確接收。
- 實現 可靠傳輸(通過 ACK 確認機制)
- 規則:
- 若接收方收到序號=0、長度=100 的數據,則發回的確認序號=100。
- 僅當 ACK 標志位=1 時有效(如普通數據報的 ACK 或純確認報文)。
- 作用:
-
4位首部長度
- 作用:
- 指示 TCP 首部的 總長度(單位為 4字節)。
- 首部的 總長度 = 固定20字節 + 選項長度(0~40字節)
- 計算方式:
- 首部長度 = 4位值 × 4(字節)。
- 最小值=5(即 20 字節,無選項),最大值=15(即 60 字節)。
- 示例:
- 若 4 位值為 1010(十進制 10),則首部長度=10×4=40 字節。
- 作用:
-
16 位窗口大小
- 表示該報文發送端的接收窗口的大小(單位是字節)
- 接收窗口在TCP協議層,在接受緩沖區,接收窗口大小 = 接收緩沖區總大小 - 已占用緩沖區大小。
- 接收方通過窗口大小告知發送方 當前可接收的數據量(單位:字節),防止發送方速率過快導致接收方緩沖區溢出。
- 發送方必須保證 未確認的數據量 ≤ 接收方通告的窗口大小。 未確認的數據 是指發送方已發出但尚未收到對應ACK報文的數據
- 對于發送方來講,發送速度由對方的接受緩沖區中剩余空間的大小決定!
-
6位標志位
- 6個標記位:區分了報文的類型
- 每一個標記位都是一個bit,如果這一位為1,則表示這一位的標記位有效;為0,則反之。
- ACK: 確認序號是否有效,用于應答報文(ACK為1,則有效)
- PSH: 發送端發送,提示接收端應用程序(應用層)立刻從TCP緩沖區(傳輸層)把數據讀走
- FIN: 通知對方, 本端要關閉了。(發送完這個報文,本端的寫端關閉,讀端未關,用于四次揮手的第一次揮手)
- SYN:表示請求建立新的TCP連接。用于三次握手的前兩次握手。
- URG:緊急指針標志,表示該報文段中存在緊急數據,需要優先處理。
- 緊急指針(Urgent Pointer):
這是一個16位的偏移量,與序列號(Sequence Number)結合使用,指向緊急數據的末尾位置。接收方會根據緊急指針快速定位找到并處理緊急數據(如中斷命令)。
- 緊急指針(Urgent Pointer):
- RST:復位標志,表示發送方要求立即重新連接。
- 當連接斷開時,B不知情,還在向A發送消息,A就會發送RST,要求重新連接
7.1.1 為什么一個TCP報文中同時需要序號和確認序號?
- 核心原因:全雙工通信
- TCP是全雙工協議,即通信雙方可以同時發送和接收數據。因此,單個報文需要:
- 序號:標識本方發送的數據的字節流位置。
- 確認序號:確認對方發送的數據的接收情況。
7.1.2 TCP為什么是面向字節流的,UDP是面向數據報呢?
- TCP:
- 流程:
- 發送方:send()是把要發送的數據拷貝進入了傳輸層的發送緩沖區中,然后由操作系統自己來分配數據到每個報文中,然后發送報文;
- 接收方:操作系統自己接收報文并把報文分離,提取數據,放入接收緩沖區中,接收方通過recv()得到接收緩沖區的數據。
- 字節流抽象:
- TCP 將數據視為連續的字節序列,而非獨立報文。發送方和接收方通過序號(Sequence Number)跟蹤字節位置,確保數據按序到達
- 無數據邊界
- 發送方:多次調用 send() 寫入的數據會被合并為一個連續的字節流。
- 接收方:調用 recv() 時可能一次性讀取多個發送端 send() 的內容,或分多次讀取一個 send() 的內容。
- 流程:
- UDP:
- 流程:
- 發送方:sendto()直接將用戶數據加上UDP首部(8字節)形成數據報,直接交給IP層發送;
- 接收方:操作系統自己接收報文并把報文分離,提取數據,放入接收緩沖區中,但是是按照一個報文一個報文的分離好數據,recvfrom()接收只能接收一個報文的數據
- 數據報抽象:
- UDP 將每個 send() 調用視為一個獨立報文,接收方必須按報文邊界讀取。
- 保留數據邊界
- 發送方:每次 send() 對應一個 UDP 數據報。
- 接收方:每次 recv() 讀取一個完整的報文,若緩沖區小于報文大小,多余數據會丟失。
- 流程:
7.2 確認應答(ACK)機制
應答:
- 接收方每接收到一條報文,都要向發送方發送一條應答。(應答也是報文)報文多時,應答是一大批一起發送的。例如,接收了10個報文之后,然后再一起發送10一個應答。
- 應答(ACK)的本質:
作用:接收方通過 ACK 告知發送方“數據已成功接收”,并指示期望接收的下一個字節序號。 - 核心字段:
ACK 標志位:置 1 時表示該報文是應答。
確認序號:值為最后一個有序接收的字節序號 + 1
7.3 超時重傳機制
TCP 是可靠傳輸協議,保證數據一定能到達對端。如果發送的數據包丟失了,TCP 會通過 超時重傳 機制重新發送丟失的報文段。下面詳細講解它的工作原理。
- 為什么需要超時重傳?
- 網絡不可靠:IP 層不保證數據包一定能到達,可能會因為擁塞、鏈路故障、路由器丟包等原因丟失。
- TCP 必須保證可靠傳輸,所以要有機制檢測丟包并重傳。
- 超時重傳的基本流程
- 發送方發送數據包,并啟動一個 重傳計時器(Retransmission Timer)。
- 等待 ACK:
- 如果接收方成功收到數據,會回復 ACK(確認)。
- 如果 ACK 在超時時間內到達,發送方取消計時器,繼續發下一個數據包。
- 如果超時未收到 ACK:
- 發送方認為數據包丟失,重傳該數據包。
- 同時調整超時時間(通常加倍,避免頻繁重傳加劇網絡擁塞)。
- 超時重傳時間(RTO)不宜太短或太長
- 如果 RTO 設置得太短,可能在 ACK 還在路上時,就誤判丟包并重傳。
- 如果 RTO 設置過長,即使真的丟包,也要等很久才重傳。降低了效率
7.4 TCP三次握手
-
狀態變化
- 客戶端狀態流:
CLOSED → SYN-SENT → ESTABLISHED - 服務器狀態流:
CLOSED → LISTEN → SYN-RCVD → ESTABLISHED
- 客戶端狀態流:
-
在TCP三次握手中,客戶端序列號從x變為x+1的原因
- 關鍵原因:SYN標志位消耗序列號
- TCP協議規定:
- 任何帶有SYN或FIN標志的TCP報文,即使不攜帶應用數據,也會使序列號+1
- 這是因為SYN和FIN都被視為需要確認的"邏輯數據"
-
listen() - 服務器準備接收連接
- 服務器調用listen()后,進入LISTEN狀態,開始監聽指定端口的連接請求。
- 它并不直接參與三次握手,而是為握手提供條件:內核會為監聽端口維護一個未完成連接隊列(SYN隊列)和一個已完成連接隊列(ESTABLISHED隊列)。
- 如果沒有listen(),服務器即使收到SYN包也會直接丟棄
- listen()在握手開始前調用,是服務器能夠響應客戶端SYN的前提條件。
7.4.1 連接管理機制
在 TCP 三次握手過程中,服務器內核維護 兩個關鍵隊列 來管理連接狀態:
- SYN 隊列(半連接隊列,syns queue)
- 存儲已收到 SYN(第一次握手),但未完成三次握手的連接。
- 這些連接處于 SYN_RCVD 狀態。
- ACCEPT 隊列(全連接隊列,accept queue)
- 儲已完成三次握手(ESTABLISHED),但尚未被 accept() 取出的連接。
三次握手中 SYN 隊列 和 ACCEPT 隊列 的交互流程:
- 第一次握手(客戶端connext()):
- 服務器收到 SYN,將該連接放入 SYN 隊列
- 第二次握手
- 第三次握手
- 客戶端:三次握手已經完成,客戶端那邊可以開始通信了
- 服務端:看 ACCEPT 隊列有沒有位置。
-
沒有位置(即服務端沒有調用accept() 或者 調用了accept(),但是SYN 隊列前面還有連接,沒有輪到它)
- Linux 默認行為:直接丟棄 ACK,不完成第三次握手,連接保留在 SYN 隊列,等待ACCEPT 隊列位置。—— 還在SYN 隊列,三次握手沒完成
- 客戶端發起請求,會收不到響應,然后超時重傳 ACK。如果說持續較長時間沒完成三次握手的話,客戶端放棄重傳(連接超時失敗)。
- 如果 net.ipv4.tcp_abort_on_overflow=1,服務器會直接回復 RST 重置連接。
-
有位置,并且該連接在SYN 隊列頭,可以占據該位置
- 內核將該連接從 SYN 隊列 移至 ACCEPT 隊列中 —— 到達 ACCEPT 隊列,三次握手完成
-
- 所以對于服務端來說,該連接進入了ACCEPT 隊列,即三次握手完成
- 兩端三次握手完成,服務端是否發起accept()
- 不發起或者發起了,但還沒輪到該連接。—— 該連接仍然在TCP協議層
- 客戶端可以正常發送數據,且 不會收到 RST 重置連接(只要連接在 ACCEPT 隊列 中)。
- 服務端內核會接收數據并緩存,但應用層無法讀取(直到 accept() + read())。服務端也無法發送數據,但可以發送不帶數據的報文。
- 如果服務端的接收窗口滿了,服務端就會發送報文,提醒客戶端控制發消息,客戶端就會阻塞或丟包。( TCP 流量控制機制)
- 發起了,并且該連接在ACCEPT 隊列頭
- ACCEPT 隊列刪除連接,該連接被提取到應用層(其實是應用層拿到該連接的管理權)應用層通過accept()得到通信套接字,可用于數據傳輸。
- 不發起或者發起了,但還沒輪到該連接。—— 該連接仍然在TCP協議層
小知識點:
- SYN 隊列(半連接隊列)不會長期維護未完成的連接
- listen的第二個參數為backlog,backlog+1表示全連接隊列的最大連接長度。
int listen(int sockfd, int backlog);
SYN 洪水攻擊:
-
什么是 SYN 洪水攻擊?
- SYN 洪水是一種 拒絕服務攻擊(DoS/DDoS),攻擊者利用 TCP 三次握手的機制缺陷,偽造大量虛假的 SYN 包 發送給目標服務器,消耗其資源(如半連接隊列和內存),導致服務器無法處理正常用戶的連接請求。
-
SYN 洪水攻擊流程:
- 攻擊者偽造大量 SYN 包:
- 使用虛假 IP(如隨機源地址)發送 SYN,不回復 ACK。(不給第三次握手)
- 服務器資源被耗盡:
- 每收到一個 SYN,服務器分配內存并回復 SYN-ACK,連接滯留在半連接隊列。
- 由于SYN隊列滿了,正常用戶的 SYN 被丟棄,服務癱瘓。
- 攻擊者偽造大量 SYN 包:
7.4.2 為什么握手是三次
TCP采用三次握手(3-way handshake)建立連接,而不是一次、兩次、四次或更多次,這是為了在可靠性和效率之間取得最佳平衡。以下是詳細解釋:
-
為什么不能是「一次握手」?
- 問題:客戶端發送連接請求后直接開始傳輸數據,服務端無法確認自己是否準備好了。
- 風險:
- 服務端可能未準備好接收數據(資源未分配)。
- 網絡中的延遲或重復的舊連接請求(歷史報文)可能導致服務端誤判。
-
為什么不能是「兩次握手」?
- 表面看:客戶端發送請求(SYN),服務端回復確認(SYN-ACK),似乎足夠了。
- 實際缺陷:
- 無法防止歷史連接問題:如果客戶端的第一個SYN因網絡延遲很久才到達服務端(舊連接的SYN),服務端會直接建立連接,但客戶端可能早已放棄,導致服務端資源浪費。
- 無法確認客戶端的接收能力:服務端不知道客戶端是否能收到自己的SYN-ACK,若客戶端未收到,服務端會一直等待
-
為什么「三次握手」是完美的?
- 關鍵作用:
- 雙方確認彼此的發送和接收能力。
- 同步初始序列號(ISN),保證數據順序。
- 防止資源被無效歷史連接占用。
- 三次是>=3的最小奇數次,最后一定是最開始發送連接請求的客戶端發送最后一條報文,即服務端接收最后一條報文
- 如果第三次握手的報文丟失,客戶端三次握手成功,形成連接,服務端失敗,不形成連接,那么資源消耗的代價是在客戶端,而不是服務端。
- 如果客戶端后續發送數據(而不僅是ACK):服務端收到非SYN報文時,會回復RST復位報文(因為其連接未建立)。
- 客戶端收到RST后,會立即釋放連接。
- 關鍵作用:
-
為什么不需要四次或五次?
- 三次已足夠:三次握手后,雙方已完全確認通信能力,更多次數不會帶來額外好處。
- 效率問題:更多握手次數會增加延遲和開銷,但不會提高可靠性。
7.5 TCP 四次揮手
- MSL:報文在網絡中,最大生存時間
- 主動斷開連接的一方,在4次揮手完成之后,要進入time-wait狀態,等待若干時長,之后,自動釋放
- 為什么要等待 2MSL 才釋放?
主要有 4個原因:- (1) 確保最后一個ACK能到達對方
- 如果主動關閉方最后發送的ACK丟失,對方(被動關閉方)會重傳FIN。
- TIME_WAIT的存在使得主動關閉方可以再次發送ACK,避免對方一直處于LAST_ACK狀態。
- (2) 讓網絡中殘留的舊報文失效
- TCP報文可能在網絡中因延遲而滯留(如路由器抖動),如果相同的四元組(源IP、源端口、目標IP、目標端口)的新連接建立,可能會收到舊連接的臟數據。
- 等待2MSL可以確保所有屬于舊連接的報文都從網絡中消失。
- (3) 保證TCP全雙工可靠關閉
- 確保雙方都能正常完成關閉流程,避免一方因丟包導致連接未正確終止。
- (4) 兼容不可靠網絡
- 在網絡不穩定的環境下,TIME_WAIT能減少因丟包或亂序導致的新連接數據錯亂問題。
- (1) 確保最后一個ACK能到達對方
7.6 滑動窗口
因為有滑動窗口區域,我們才可以一次向對方發送大量的tcp報文!
7.6.1 滑動窗口的結構
滑動窗口是發送方的發送緩沖區的一部分。
7.6.2 快重傳
快重傳是TCP的一種丟包恢復機制,用于在檢測到數據包丟失時快速觸發重傳,而無需等待超時,從而減少延遲、提高傳輸效率。
- 快重傳的核心原理
當發送方連續收到 3個重復的ACK 時,立即重傳丟失的單個數據包(無需等待超時)。如同所示:
快重傳:快速檢測并修復單包丟失,避免等待超時,提高傳輸效率。
超時重傳:處理嚴重丟包或連接中斷等快重傳無法覆蓋的場景。(即發送方沒法連續收到 3個重復的ACK的時候)。
7.7 延遲應答
7.7.1 延遲應答的概念
- “延遲應答”就是接收方收到數據后,故意等一等再回復ACK(確認消息),目的是減少網絡中小ACK包的數量,提高傳輸效率。
- 網絡傳輸中,數據確實是被分割成一個個報文/包(Packet)發送的,可以選擇 “逐包發送” 或者 “批量發送” 。“批量發送”:減少頭部開銷,提高吞吐量。
通俗版詳解:
想象你網購收快遞:
- 沒有延遲應答(普通模式)
快遞員(發送方)每送一個包裹(數據包),你就(接收方)必須立刻喊一聲:“收到啦!”(ACK)。
問題:如果快遞員連續送10個包裹,你要喊10次“收到啦!”,很累且浪費力氣(網絡帶寬)。
- 開啟延遲應答(優化模式)
快遞員送第一個包裹時,你開始憋著不吭聲(啟動200ms延遲計時器)。
如果200ms內他又送第二個包裹:你直接喊“兩個都收到啦!”(合并ACK,即把多條ACK打包在一起形成一個大包)。
如果200ms內他沒送新包裹:你超時后喊“第一個收到啦!”(單獨發ACK)。
7.7.2 延遲應答的作用和觸發條件
-
延遲應答的作用
-
減少ACK報文數量:
若每次收到數據都立即回復ACK,會導致大量小包(如40字節的純ACK)占用帶寬。
示例:發送方連續發送10個數據包,立即ACK會生成10個小包;延遲ACK可能合并為1個ACK。 -
提高網絡利用率:
合并ACK可減少網絡擁塞,尤其在高延遲或低帶寬環境中(如移動網絡)。 -
觸發捎帶應答:
延遲期間若接收方有數據要發送(如HTTP響應),可將ACK“捎帶”在數據包中,完全避免單獨發送ACK。 -
延遲應答通過暫緩發送確認(ACK),為應用程序爭取時間從緩沖區讀取數據。這使得接收方能夠向發送方通告一個更大的可用窗口(RWND),從而允許發送方一次性發送更多數據,顯著提升網絡吞吐效率和帶寬利用率。
-
-
延遲應答的觸發條件
接收方在以下任一條件滿足時發送ACK:- 超時時間到:通常延遲200ms(Linux默認值)。
- 收到兩個數據包:即使未超時,收到第二個包后必須立即ACK(RFC 1122規定)。
- 有數據需要發送:直接捎帶ACK。
7.8 流量控制
流量控制(Flow Control) 是一種機制,其根本目的是防止發送方發送數據過快、過多,導致接收方來不及處理,最終造成數據丟失。
它是一種點對點的(通常是接收方控制發送方)、保證可靠性的機制。
一個生動的比喻:水池與水龍頭
想象一個水池(接收方的緩沖區)和一個水龍頭(發送方)。
- 正常情況:你打開水龍頭,水流入水池,同時水池的排水管也在排水。進水和排水速度相當,水池不會滿。
- 問題出現:如果水龍頭開得太大(發送方發送太快),而排水管很細(接收方處理能力慢),水池里的水就會越積越多。
- 最終結果:如果不加控制,水池最終會滿溢,水會漫出來造成浪費(數據包丟失)。
- 流量控制的作用:水池上有一個水位刻度尺(接收窗口)。當水位過高時,水池會向水龍頭發送一個信號:“水位高了,關小一點!”(通告一個較小的窗口值)。水龍頭收到信號后就調小水流(降低發送速率)。當水位下降后,水池又說:“現在可以開大一點了”(通告一個較大的窗口值)。
- 這個“根據水池水位動態調整水龍頭大小”的過程,就是流量控制。
實際的過程:
- 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過ACK端通知發送端;
- 窗口大小字段越大, 說明網絡的吞吐量越高;
- 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
- 發送端接受到這個窗口之后, 就會減慢自己的發送速度;
- 如果接收端緩沖區滿了, 就會將窗口置為0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端.
7.9 擁塞控制
我們通過理解cwnd(擁塞窗口)來理解擁塞控制。
cwnd(擁塞窗口)就是:
- 發送方心里的一套“交通規則”,它規定了自己一次最多能往馬路上扔多少輛車(數據包),怕的是把路給堵死了。
- 每一個發送者都有cwnd(擁塞窗口),并且是自己通過cwnd來控制自己的發送,而不是通過網絡統一控制,網絡沒有這個能力。接收方也管不著發送方的cwnd。
舉例說明,擁塞控制的過程。
現在,你(發送方)是個車隊經理,你的任務就是往這條公路上發車。
如果沒有 cwnd(瘋狂經理):
你根本不管路上堵不堵,一口氣把你所有的車(比如 1000 輛)全派上去。
結果就是:所有車在第一個路口就堵死了,誰也動不了。這就是網絡擁塞。
有了 cwnd(聰明經理):
你不知道這條路有多寬,所以你得很小心。
- 第一步(慢啟動): 你先派 1輛車 去探路。車順利到達后,對方會回復你:“收到1號車了!”(這叫 ACK)。
你一收到這個回復,心里就想:“哦?路是通的?那我這次派 2輛車!”
2輛車都到了,對方回復兩個確認。你就想:“太棒了!這次派 4輛車!”
這就是 cwnd 在增長:1 -> 2 -> 4 -> 8… 它代表了你一次性能派出去的車隊規模。 - 第二步(擁塞避免): 當車隊規模大到一定程度(比如快到你知道的某個路口容量極限了ssthresh),你就不敢翻倍派車了。改成一次只多派1輛車:8輛 -> 9輛 -> 10輛… 慢慢試探。
- 第三步(發現堵車):通過兩種“信號”,你發現堵車了。不同信號,不同措施.
- 信號一:超時重傳 (Timeout) —— “徹底失聯”
- 情景比喻:你派出一隊車(比如10輛)。結果,連第一輛車的確認消息都沒回來。即你沒有收到一個ACK。
- 發送方的判斷:“完了!這已經不是普通擁堵了,這怕是重大交通事故,路完全斷了(比如路由器隊列滿了開始丟包,或者鏈路中斷)!連個信都傳不回來。”
- 發送方的反應(非常嚴厲):
1. 大幅縮減車隊規模:直接把 cwnd 降為 1(cwnd = 1)。回到最初的起點。
2. 降低預期:把慢啟動閾值 ssthresh 設為當前擁塞窗口的一半(ssthresh = cwnd / 2)。
3. 重新慢啟動:從 cwnd=1 開始,像剛開始一樣,指數增長,重新探路。 - 特點:反應劇烈,效率較低,但用于處理最嚴重的網絡問題。
- 信號二:重復ACK (Dup ACK) —— “收到投訴電話”
- 情景比喻:你收到了連續三個相同的ACK。
- 發送方的判斷:“哦!對方已經收到2號車之后的數據了,但唯獨3號車沒送到!這說明網絡可能只是輕度擁堵,丟了個別包,但路沒完全斷,后續的車隊(4,5,6號)還是能到達的。”
- 發送方的反應(快速重傳/恢復):
- 調整規模:它認為網絡只是部分擁堵,所以反應溫和一些。
把慢啟動閾值 ssthresh 設為當前 cwnd 的一半(ssthresh = cwnd / 2)。
但 cwnd 不會重置為1! 而是被設置為新的 ssthresh 值(ssthresh 有的可能還會加3,因為收到了3個重復ACK)。 - 直接進入擁塞避免:因為cwnd==ssthresh ,所以直接進入線性增長的擁塞避免階段,而不是慢啟動。
- 立刻重傳:它不會傻等到超時,而是立即把懷疑丟失的3號車重新發出去。這就是“快速重傳”。
- 調整規模:它認為網絡只是部分擁堵,所以反應溫和一些。
- 特點:反應迅速且溫和,避免了超時重傳帶來的性能暴跌,大大提高了效率。
- 信號一:超時重傳 (Timeout) —— “徹底失聯”
也可以發現,慢啟動閾值 ssthresh 等于 最近一次發生擁塞控制時, cwnd 的一半(ssthresh = cwnd / 2)
7.10 粘包問題
那么所謂的“粘包”現象是什么?
兩種常見情況:
- 多個消息粘在一起:發送方快速連續發送了兩個消息 MessageA 和 MessageB,接收方可能一次 recv 就讀到了 MessageA + MessageB。
- 一個消息被拆開:發送方發送了一個較大的消息 BigMessage,TCP 可能會將其拆分成多個數據包傳輸,接收方可能需要多次 recv 才能收齊整個消息。
所以“粘包”根本不是一個問題,粘包只是一種現象,UDP絕對沒有“粘包”現象。
如何處理粘包問題?
既然 TCP 不管消息邊界,那就必須由應用層自己來定義消息的邊界。這是網絡編程中設計應用層協議的關鍵。
常見的解決方案有:
- 定長消息
每個發送的消息都是固定的長度。那么我們就可以把接受到的一大穿字符串以固定長度分割成一個個消息。例如,規定每個消息都是 100 字節。如果不足,就用空格或 \0 填充。 - 使用特殊分隔符
在每個消息的末尾加上一個特殊的字符或字符串作為結束標記,例如換行符 \n。 - 長度前綴(最常用、最推薦的方法)
在消息體的前面,加上一個固定長度的字段(Header,頭部),用來表示后面消息體(Body)的長度。
[ 4 字節的消息長度 ] [ 實際的消息數據 ](Header) (Body)例如,要發送 "Hello World",其長度為 11。發送的數據結構為:
[0x00, 0x00, 0x00, 0x0B] [H, e, l, l, o, , W, o, r, l, d]
7.10 TCP的異常退出
核心原則
TCP通過握手(SYN) 和揮手(FIN/RST) 來管理連接的生命周期。但這些報文也是普通的網絡數據包,在極端異常情況下(如斷電、斷網)根本無法發出。因此,TCP需要一套機制來探測和清理這些“僵死”的連接。
分為兩種情況。
第一種情況:進程終止或機器正常重啟(有序關閉)
無論是進程調用 close() 退出,還是其他進程退出方式,或者操作系統正常重啟,內核的協議棧都會完成標準的TCP四次揮手過程。
kill -9
退出進程是例外。kill -9(不是ctr + c
)退出進程的話,不會進行4次揮手,連接處理是第二種情況了。
第二種情況:機器掉電、宕機或網絡中斷(無序中斷)
背景:通信一方宕機/斷電(對端不知情)。假設服務器突然斷電,客戶端完全不知情。
- 階段一:客戶端不知情,連接看似存在
在客戶端看來,TCP連接狀態依然是 ESTABLISHED。它不知道服務器已經“掛了”。 - 階段二 :分兩種情況
- 客戶端一直沒有發消息,啟動保活機制
- TCP內置了一個可選的 保活定時器
- 連接空閑(無數據交換)超過 tcp_keepalive_time(默認7200秒,即2小時)后,保活機制啟動。
- 客戶端開始發送保活探測包。這個包就是一個空的、序列號是對方期望序號-1的ACK包(純粹是為了引發響應)。
- 發現服務器已崩潰:服務器無法響應。客戶端在連續發送9次探測包(總計約 75 * 9 ≈ 11分鐘)后都收不到任何ACK回復,則判定連接已死亡。
- 連接清理:客戶端內核會將本地的TCP連接狀態置為 CLOSED,并釋放資源。
- 客戶端向服務端發送數據(更快發現錯誤)
- 數據包在網絡上根本無法到達服務器主機。它可能在某個路由器上就被丟棄了,然后超時重傳,然后到重傳一定次數判定連接失敗。
- 連接清理:客戶端內核會將本地的TCP連接狀態置為 CLOSED,并釋放資源。
- 客戶端一直沒有發消息,啟動保活機制