Linux網絡基礎(中)

目錄:

再談“協議”

HTTP協議?

認識URL:

urlnecode和urldecode?

HTTP協議格式:

HTTP的方法:

簡易HTTP服務器:?

?傳輸層

再談端口號:?

端口號范圍劃分:

netstat:

pidof:

UDP協議

UDP協議端格式?:

檢驗和的解釋:

UDP的特點:

面向數據報:

UDP的緩沖區:

UDP使用注意事項:

基于UDP的應用層協議:

TCP協議?

TCP協議段格式:?

??編輯

超時重傳機制:?

連接管理機制:

理解TIME_WAIT狀態:

滑動窗口:?

流量控制:

擁塞控制:

延遲應答:?

捎帶應答:

?面向字節流:

?粘包問題:

?TCP異常情況

TCP小結:

基于TCP的應用層協議:

TCP/UDP對比?

用UDP實現可靠傳輸(經典面試題)

TCP的相關實驗?

理解listen的第二個參數??

Linux網絡編程套接字(上)icon-default.png?t=N6B9https://blog.csdn.net/Obto_/article/details/132189802?


再談“協議”

協議是一種 "約定". socket api的接口, 在讀寫數據時, 都是按 "字符串" 的方式來發送接收的. 如果我們要傳輸一些"結構化的數據" 怎么辦呢?

方案一:

  • 客戶端發送一個"1+1"的字符串
  • 這個字符串中會有兩個操作數且都是整形
  • 兩個數據之間會有一個字符是運算符
  • 數字和運算符之間沒有空格
  • ....?

方案二:

  • 定義結構體來表示我們需要交互的信息
  • 發送數據時將這個結構體轉化成字符串,接收到的數據的時候再用相同的規則把字符串轉成結構體
  • 這個過程叫做“序列化”和“反序列化”

無論我們采用方案一還是方案二,亦或者其他的,其目的都是保證一端發送時夠早的數據,在另一端能夠正確的進行解析,這種約定就是應用層協議?

HTTP協議?

雖然說應用層協議可以由我們程序員自己來定,但實際上,已經有大佬定義了現成的,又非常好用,HTTP(超文本傳輸協議)就是其中之一?

認識URL:

平時我們俗稱的“網址”,其實就是URL

?

urlnecode和urldecode?

像 / ? : 等這樣的字符, 已經被url當做特殊意義理解了. 因此這些字符不能隨意出現.
比如, 某個參數中需要帶有這些特殊字符, 就必須先對特殊字符進行轉義.
轉義的規則如下:
將需要轉碼的字符轉為16進制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式

?

?比如我搜索:c++那么這兩個++就會被轉意成"%2B%2B"

?

HTTP協議格式:

//使用該指令可以在linux下查看url的請求
curl -I www.baidu.com

?

?

  • 首行:[方法] + [url] + [版本]
  • Header:請求的屬性,冒號分隔的禁止對,每組屬性之間用\n分隔,遇到空行表示Header結束?
  • Body:空行后面的內容都是Body(上圖沒有把Body截圖進去,太長了),Body允許為空,但如果Body存在,則在Header會有一個Content-Length屬性來表示Body的長度?

?

ps:Header中有的屬性不止圖上這些,這些只是較為常見的...?

HTTP的方法:

?

方法? ? ? ? ? ? ??說明支持的HTTP協議版本

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要求用隧道協議鏈接代理1.1
LINK建立和資源之間的聯系1.0
UNLINK斷開鏈接關系1.0

HTTP的狀態碼?:

類別原因
1XXInformational(信息狀態碼)接受的請求正在處理
2XXSUCCESS(成功狀態碼)請求正常處理完畢
3XXRedirection(重定向狀態碼)需要進行附加操作以完成請求
4XXClient Error(客戶端錯誤狀態碼)服務器無法處理請求
5XXServer Error(服務器錯誤狀態碼)服務器處理請求出錯

常見的狀態碼:200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向)

504(Bad Gateway)?

HTTP常見的Header:

  • ?Content-Type: 數據類型(text/html等)
  • Content-Length: Body的長度
  • Host: 客戶端告知服務器, 所請求的資源是在哪個主機的哪個端口上
  • User-Agent: 聲明用戶的操作系統和瀏覽器版本信息
  • referer: 當前頁面是從哪個頁面跳轉過來的
  • location: 搭配3xx狀態碼使用, 告訴客戶端接下來要去哪里訪問
  • Cookie: 用于在客戶端存儲少量信息. 通常用于實現會話(session)的功能

簡易HTTP服務器:?

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;
}

?傳輸層

負責數據能夠從發送端傳輸接收端

再談端口號:?

端口號(Port)標識了一個主機上進行通信的不同應用程序?

?在TCP/IP協議中, 用 "源IP", "源端口號", "目的IP", "目的端口號", "協議號" 這樣一個五元組來標識一個通信(可以通過netstat -n查看);

?

端口號范圍劃分:

  • 0-1023:知名端口號,HTTP,FTP,SSH等這些廣為使用的應用層協議,他們的端口號都是固定的

  • 1024-65535:OS動態分配的端口號,客戶端程序的端口號,就是由OS從這個范圍分配的

有一些服務器是非常常用的,人們約定一些常用的服務器用的都是以下這些固定端口號

  • ssh服務器:用22端口
  • ftp服務器:用21端口
  • telnet服務器:用23端口
  • http服務器:用80端口
  • https服務器用:用443端口?

執行下面的命令, 可以看到知名端口號

cat /etc/services

netstat:

netstat使用來查看網絡狀態的工具:

語法:netstat [選項]

功能:查看網絡狀態

常用選項

  • n 拒絕顯示別名,能顯示數字的全部轉化成數字
  • l 僅列出有在 Listen (監聽) 的服務狀態
  • p 顯示建立相關鏈接的程序名
  • t (tcp)僅顯示tcp相關選項
  • u (udp)僅顯示udp相關選項
  • a (all)顯示所有選項,默認不顯示LISTEN相關

pidof:

查看服務器的進程id?

語法:pidof [進程名]

功能:通過進程名查找進程id?


UDP協議

UDP協議端格式?:

?

  • 16UDP長度,表示整個數據報(UDP首部+UDP數據)的最大長度
  • 如果檢驗和出錯,就會直接丟棄?

檢驗和的解釋:

UDP的檢驗和可以幫助接收方驗證接收到的UDP數據是否完整、正確,并且未被損壞或篡改。發送方在發送UDP數據包時會計算數據包的檢驗和,并將該檢驗和值包含在UDP頭部中。接收方在接收數據包時,也會重新計算數據包的檢驗和,并將計算結果與接收到的檢驗和進行比較。如果兩個值不相等,就說明數據包在傳輸過程中發生了錯誤或篡改。?

UDP的特點:

UDP?傳輸的過程類似寄信:

  • 無連接:直到對端IP和PORT直接進行傳輸
  • 不可靠:沒有確認機制,沒有重傳機制,如果網絡故障導致該數據段無法發送到對端,UDP協議也不會給應用層返回任何的錯誤信息
  • 面向數據包:不能夠靈活的控制讀寫數據的次數和數量(一次就發一個完整的報文)

面向數據報:

應用層交給UDP多長的報文,UDP原樣發送,不會 拆分也不會合并

用UDP傳輸100個字節的數據:

  • ?如果發送端調用一次sendto, 發送100個字節, 那么接收端也必須調用對應的一次recvfrom, 接收100個字節; 而不能循環調用10次recvfrom, 每次接收10個字節

UDP的緩沖區:

  • UDP沒有真正意義上的發送緩沖區,調用sendto會直接交給內核,由該內核數據傳給網絡協議進行后續的傳輸動作

  • UDP具有接收緩沖區,但是這個接收緩沖區不能保證收到的UDP報的順序和發送UDP報的順序一致;如果緩沖區滿了,再到達的UDP數據會被丟棄

UDP使用注意事項:

我們注意到, UDP協議首部中有一個16位的最大長度. 也就是說一個UDP能傳輸的數據最大長度是64K(包含UDP首部).然而64K在當今的互聯網環境下, 是一個非常小的數字.如果我們需要傳輸的數據超過64K, 就需要在應用層手動的分包, 多次發送, 并在接收端手動拼裝

基于UDP的應用層協議:

  • NFS:網絡文件系統

  • TFTP:簡單文件傳輸協議

  • DHCP:動態主機配置協議

  • BOOTP:啟動協議

  • DNS:域名解析協議?


TCP協議?

TCP全稱為 "傳輸控制協議(Transmission Control Protocol"). 人如其名, 要對數據的傳輸進行一個詳細的控制

TCP協議段格式:?

?

  • 源/目的端口號:表示數據是從哪個進程來,到哪個進程去
  • 32位序號/32位確認號會在下文詳細講述
  • 4位TCP報頭長度:表示該TCP頭部有多少個32位bit(有多少字節);所以TCP頭部最大長度是15*4
  • 6位標志位:
    • URG:緊急指針是否有效
    • ACK:確認號是否有效
    • PSH:提示接收端應用立刻從TCP緩沖區把數據讀走
    • RST:對方要求重新建立連接;我們把攜帶RST表示的稱為復位報文段
    • SYN:請求建立連接;我們把攜帶SYN標識的稱為同步報文段
    • FIN:通知對方,本端要關閉了,我們稱攜帶FIN標識的為結束報文段
  • 16位窗口大小:后面詳細講
  • 16位校驗和:發送端填充,CRC校驗。接收端校驗不通過,則認為數據有問題。
  • 16位緊急指針:表示那部分數據是緊急數據

?CRC校驗:

CRC校驗的原理如下:

  1. 首先,定義一個生成多項式(通常是二進制數),表示為G(X)(如0x8005)。
  2. 發送方計算數據的校驗碼,使用生成多項式G(X)進行計算。具體計算過程是將數據按照二進制形式做除法運算,除數為生成多項式G(X)。
  3. 將計算得到的校驗碼添加到數據后面,形成帶有校驗碼的數據包,然后發送給接收方。
  4. 接收方使用相同的生成多項式G(X)進行計算,將接收到的數據進行除法運算,得到一個余數。
  5. 如果接收方計算得到的余數為0,則說明數據在傳輸過程中沒有發生錯誤;如果余數不為0,則說明數據發生了錯誤或者被篡改。

?

?

TCP將每個字節的數據都進行編號,即序列號

?

?每一個ACK都帶有對應的確認序列號,意思就是告訴發送者,我已經收到了這個序列號之前的所有數據,下一次你從這個序列號+1的后面開始發送


超時重傳機制:?

?

  • 主機A發送數據給B之后,可能因為網絡擁堵等問題,數據無法到達B
  • 如果主機A在一個特定時間間隔內沒有收到B發來的確認應答,就會重發?

當然還有下面這種情況:

?

因此主機B會收到很多重復數據. 那么TCP協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉.這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果
?

那么超時的時長如何定義:

  • 最理想的情況下, 找到一個最小的時間, 保證 "確認應答一定能在這個時間內返回
  • 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的
  • 如果超時時間設的太長, 會影響整體的重傳效率
  • 如果超時時間設的太短, 有可能會頻繁發送重復的包

?TCP為了保證無論在任何環境下都能比較高性能的通信,因此會動態計算最大超時時間.

  • ?Linux中(BSD Unix和Windows也是如此), 超時以500ms為一個單位進行控制, 每次判定超時重發的超時時間都是500ms的整數倍
  • 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳
  • 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增
  • 累計到一定的重傳次數, TCP認為網絡或者對端主機出現異常, 強制關閉連接
    ?

連接管理機制:

?

服務端的狀態轉換:?

  • [CLOSED -> LISTEN]服務器端調用listen后進入LISTEN狀態,等待客戶端連接
  • [LISTEN->SYN_RCVD]一旦監聽到連接請求(同步報文段),就將該連接放入內核等待隊列中,并向客戶端發送SYN確認報文
  • [SYN_RCVD -> ESTABLISHED]服務端一旦收到客戶端的確認報文,就進入ESTABLISHED狀態,可以進行讀寫數據了
  • [ESTABLISHED -> CLOSE_WAIT]當客戶端主動關閉(close()),服務器會收到結束報文段,服務器返回確認報文段并進入CLOSE_WAIT
  • [CLOSE_WAIT -> LAST_ACK]進入CLOSE_WAIT后說明服務器準備關閉連接(需要處理完當前的數據);當服務器真正調用close關閉連接時,會響客戶端發送FIN,此時服務器進入LAST_ACK狀態,等待最后一個ACK到來(這個ACK是客戶端確認收到了FIN)
  • [LAST_ACK -> CLOSED]服務端收到了對FIN的ACK,徹底關閉連接

客戶端的狀態轉換:??

  • [CLOSED -> SYN_SENT] 客戶端調用connect, 發送同步報文段
  • [SYN_SENT -> ESTABLISHED] connect調用成功, 則進入ESTABLISHED狀態, 開始讀寫數據
  • [ESTABLISHED -> FIN_WAIT_1] 客戶端主動調用close時, 向服務器發送結束報文段, 同時進入FIN_WAIT_1
  • [FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務器對結束報文段的確認, 則進入FIN_WAIT_2, 開始等待服務器的結束報文段
  • [FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務器發來的結束報文段, 進入TIME_WAIT, 并發出LAST_ACK
  • [TIME_WAIT -> CLOSED] 客戶端要等待一個2MSL(Max Segment Life, 報文最大生存時間)的時間, 才會進入CLOSED狀態
    ?

理解TIME_WAIT狀態:

可以做一個測試,首先啟動server,然后啟動client,再將Ctrl-C是server終止后再次運行server

就會綁定失敗:

$ ./server

bind error : Address already in use?

這是因為雖然server的應用程序終止了,但是TCP協議層的連接并沒有完全斷開,因此不能再監聽同樣的server端口?

  • ?TCP協議規定,主動關閉連接的一方要處于TIME_ WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態
  • 我們使用Ctrl-C終止了server, 所以server是主動關閉連接的一方, 在TIME_WAIT期間仍然不能再次監聽同樣的server端口
  • MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同, 在Centos7上默認配置的值是60s
  • 可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值

但是為什么是2MSL?

  • ?MSL是TCP報文的最大生存時間, 因此TIME_WAIT持續存在2MSL的話
  • 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失
  • 同時也是在理論上保證最后一個報文可靠到達(假設最后一個ACK丟失, 那么服務器會再重發一個FIN. 這時雖然客戶端的進程不在了, 但是TCP連接還在, 仍然可以重發LAST_ACK);
    ?

在server的TCP連接沒有完全斷開之前不允許監聽,某些情況不太合理,下面是解決方法?

  • ?使用setsockopt()設置socket描述符的 選項SO_REUSEADDR為1, 表示允許創建端口號相同但IP地址不同的多個socket描述符
int opt = 1;
setsockopt(listenfd , SOL_SOCKET , SO_REUSERADDR, &opt ,sizeof(opt));

滑動窗口:?

剛才我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個ACK確認應答. 收到ACK后再發送下一個數據段.這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候
?

但是這樣一收一發的效率很慢,(就像你去超市買菜,跑一趟就買一根,來回跑1000趟一樣)

但是如果我們一次性發送多條數據就可以大大提升效率(指的是你一次多帶點菜回來)?

?

  • 窗口大小指的是無需等待確認應答而可以繼續發送數據的最大值. 上圖的窗口大小就是4000個字節(四個段)
  • 發送前四個段的時候, 不需要等待任何ACK, 直接發送;
  • 收到第一個ACK后, 滑動窗口向后移動, 繼續發送第五個段的數據; 依次類推
  • 操作系統內核為了維護這個滑動窗口, 需要開辟 發送緩沖區 來記錄當前還有哪些數據沒有應答; 只有確認應答過的數據, 才能從緩沖區刪掉
  • 窗口越大, 則網絡的吞吐率就越高
    ?

?

那么再這種情況出現丟包,該如何重傳?

?情況一:數據包已經到達,ACK丟失了

?

這種情況問題不大,因為可以通過后續的ACK來確認?

情況二:數據包直接就丟失了?

?

  • 當某一段報文段丟失之后, 發送端會一直收到 1001 這樣的ACK, 就像是在提醒發送端 "我想要的是 1001"一樣
  • 如果發送端主機連續三次收到了同樣一個 "1001" 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送
  • 這個時候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因為2001 - 7000)接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中
  • 當出現三次重復的確認應答就會進行重發
    ?

這種機制也被稱作"高速重發控制"(也稱“快重傳”)

流量控制:

?接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送,就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.因此TCP支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制(Flow Control)

  • ?接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 "窗口大小" 字段, 通過ACK端通知發送端
  • 窗口大小字段越大, 說明網絡的吞吐量越高
  • 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端
  • 發送端接受到這個窗口之后, 就會減慢自己的發送速度
  • 如果接收端緩沖區滿了, 就會將窗口置為0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端
    ?

?

接收端如何把窗口大小告訴發送端呢? 回憶我們的TCP首部中, 有一個16位窗口字段, 就是存放了窗口大小信息;那么問題來了, 16位數字最大表示65535, 那么TCP窗口最大就是65535字節么?

實際上, TCP首部40字節選項中還包含了一個窗口擴大因子M, 實際窗口大小是 窗口字段的值左移 M 位
?

擁塞控制:

?雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據,是很有可能引起雪上加霜的

TCP引入慢啟動機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據

  • 此處引入一個概念程為擁塞窗口
  • 發送開始的時候, 定義擁塞窗口大小為1
  • 每次收到一個ACK應答, 擁塞窗口加1
  • 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口

像這樣的擁塞窗口增長速度是指數級別的

  • 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍
  • 此處引入一個叫做慢啟動的閾值
  • 當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
    • 當TCP開始啟動的時候, 慢啟動閾值等于窗口最大值
    • 在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回1

少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;
當TCP通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;
擁塞控制, 歸根結底是TCP協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案
?

延遲應答:?

?如果接收數據的主機立刻返回ACK應答, 這時候返回的窗口可能比較小

  • ?假設接收端緩沖區為1M. 一次收到了500K的數據; 如果立刻應答, 返回的窗口就是500K
  • 但實際上可能處理端處理的速度很快, 10ms之內就把500K數據從緩沖區消費掉了
  • 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來
  • 如果接收端稍微等一會再應答, 比如等待200ms再應答, 那么這個時候返回的窗口大小就是1M
    ?

窗口越大,網絡吞吐量就越大,傳輸效率就越高,我們的目標是保證網絡不擁塞的情況下,盡量提高傳輸效率

那么所有的包都可以延遲應答么? 肯定也不是

  • 數量限制: 每隔N個包就應答一次(一般N=2,超時時間取200ms)
  • 時間限制: 超過最大延遲時間就應答一次
    ?

捎帶應答:

在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 "一發一收" 的. 意味著客戶端給服務器說了"How are you", 服務器也會給客戶端回一個 "Fine, thank you";那么這個時候ACK就可以搭順風車, 和服務器回應的 "Fine, thank you" 一起回給客戶端

?

?面向字節流:

?創建一個TCP的socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區;

  • ?調用write時, 數據會先寫入發送緩沖區中
  • 如果發送的字節數太長, 會被拆分成多個TCP的數據包發出
  • 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去
  • 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區
  • 然后應用程序可以調用read從接收緩沖區拿數據
  • 另一方面, TCP的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工

?由于緩沖區的存在, TCP程序的讀和寫不需要一一匹配(不同于UDP), 例如:

  • ?寫100個字節數據時, 可以調用一次write寫100個字節, 也可以調用100次write, 每次寫一個字節
  • 讀100個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100個字節, 也可以一次read一個字節, 重復100次;

?粘包問題:

  • 首先要明確, 粘包問題中的 "包" , 是指的應用層的數據包
  • 在TCP的協議頭中, 沒有如同UDP一樣的 "報文長度" 這樣的字段, 但是有一個序號這樣的字段
  • 站在傳輸層的角度, TCP是一個一個報文過來的. 按照序號排好序放在緩沖區中
  • 站在應用層的角度, 看到的只是一串連續的字節數據
  • 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包

那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界

  • 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的Request結構, 是固定大小的, 那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可
  • 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置
  • 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可)
    ?

?對于UDP來說,不存在粘包問題

  • 對于UDP, 如果還沒有上層交付數據, UDP的報文長度仍然在. 同時, UDP是一個一個把數據交付給應用層. 就有很明確的數據邊界
  • 站在應用層的站在應用層的角度, 使用UDP的時候, 要么收到完整的UDP報文, 要么不收. 不會出現"半個"的情況
    ?

?TCP異常情況

進程終止: 進程終止會釋放文件描述符, 仍然可以發送FIN. 和正常關閉沒有什么區別

機器重啟: 和進程終止的情況相同

機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行reset. 即使沒有寫入操作, TCP自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放


另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如HTTP長連接中, 也會定期檢測對方的狀態. 例如QQ, 在QQ
斷線之后, 也會定期嘗試重新連接
?

TCP小結:

?為什么TCP這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能

可靠性:

  • 檢驗和
  • 序列號
  • 確認應答
  • 超時重發
  • 連接管理
  • 流量控制
  • 擁塞控制

提高性能:

  • 滑動窗口
  • 快速重傳
  • 延遲應答
  • 捎帶應答

其他:?

  • 定時器(超時重傳定時器,保活定時器,TIME_WAIT定時器等)

基于TCP的應用層協議:

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP
  • 還有你自己寫TCP程序自定義的應用層協議?

TCP/UDP對比?

  • TCP用于可靠傳輸的情況, 應用于文件傳輸, 重要狀態更新等場景
  • UDP用于對高速傳輸和實時性要求較高的通信領域, 例如, 早期的QQ, 視頻傳輸等. 另外UDP可以用于廣播

用UDP實現可靠傳輸(經典面試題)

參考tcp的可靠性機制

  • 引入序列號,保證數據順序
  • 引入確認應答,確保對端收到了數據
  • 引出超時重傳,如果隔一段時間沒有應答,就重發數據

TCP的相關實驗?

理解listen的第二個參數?

這里將listen的第二個參數改成2,并且不調用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 個客戶端同時連接服務器, 用 netstat 查看服務器狀態, 一切正常.
但是啟動第四個客戶端時, 發現服務器對于第四個連接的狀態存在問題了

客戶端狀態正常, 但是服務器端出現了 SYN_RECV 狀態, 而不是 ESTABLISHED 狀態

這是因為, Linux內核協議棧為一個tcp連接管理使用兩個隊列:

  1. 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態的請求)
  2. 全連接隊列(accpetd隊列)(用來保存處于established狀態,但是應用層沒有調用accept取走的請求)

而全連接隊列的長度會受到 listen 第二個參數的影響,全連接隊列滿了的時候, 就無法繼續讓當前連接的狀態進入 established 狀態,這個隊列的長度通過上述實驗可知, 是 listen 的第二個參數 + 1




?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

??

?

?

?

?


?

?

?

?

?

?

?

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/35292.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/35292.shtml
英文地址,請注明出處:http://en.pswp.cn/news/35292.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Mybatis三劍客(一)在springboot中手動使用Mybatis

1、pom.xml中引入依賴【注意根據自己的spring boot版本選擇對應的mysql和mybatis版本】 <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.mybatis…

Ubantu安裝Docker(完整詳細)

先在官網上查看對應的版本:官網 然后根據官方文檔一步一步跟著操作即可 必要準備 要成功安裝Docker Desktop&#xff0c;必須&#xff1a; 滿足系統要求 擁有64位版本的Ubuntu Jammy Jellyfish 22.04&#xff08;LTS&#xff09;或Ubuntu Impish Indri 21.10。 Docker Deskto…

Redis基礎命令大全

這里寫目錄標題 第一章、Redis 命令大全1.1&#xff09;通用命令語法&#xff1a;ping語法&#xff1a;dbsize語法&#xff1a;select db語法&#xff1a;flushdb語法&#xff1a;exit 或 quit語法&#xff1a;redis-cli 1.2&#xff09;Redis 的 Key 的操作命令語法&#xff1…

【Java基礎】- JVM之Dump文件詳解

Java基礎 - JVM之Dump文件詳解 文章目錄 Java基礎 - JVM之Dump文件詳解一、什么是Dump三、為什么需要Dump分析思路 四、Dump記錄哪些內容4.1 Java dump 文件的格式和內容段格式行格式 4.2 常用分類heap dump和thread dumpheap dumpthread dump 五、如何生產Dump文件5.1 獲取hea…

Elasticsearch之kibana相關命令

1.中文分詞器相關命令 2.拼音分詞器相關命令

服務器之LNMP

lnmp的構成 L&#xff1a;linux系統,操作系統。 N&#xff1a;nginx網站服務&#xff0c;前端,提供前端的靜態頁面服務。同時具有代理,轉發的作用。 轉發&#xff1a;主要是轉發后端請求。轉發到PHP。nginx沒有處理動態資源的功能,他有可以支持轉發動態請求的模塊。 M&…

正則表達式練習

正則表達式練習 工具目的代碼運行結果 工具 pycharm 目的 https://www.77xsw.cc/fenlei/1_1/&#xff1a;第一頁的網址 https://www.77xsw.cc/fenlei/1_2/&#xff1a;第二頁的網址 ... https://www.77xsw.cc/fenlei/1_10/&#xff1a;第十頁的網址 代碼 import requests im…

REDIS主從配置

目錄 前言 一、概述 二、作用 三、缺點 四、redis主從復制的流程 五、搭建redis主從復制 總結 前言 Redis的主從配置是指在Redis集群中&#xff0c;將一個Redis節點配置為主節點&#xff08;master&#xff09;&#xff0c;其他節點配置為從節點&#xff08;slave&#xff09;…

【數據結構?堆】堆排序(理論基礎)

堆的定義  ? 堆是一個完全二叉樹   –所有葉子在同一層或者兩個連續層   –最后一層的結點占據盡量左的位置  ? 堆性質   –為空, 或者最小元素在根上   –兩棵子樹也是堆 存儲方式  ? 最小堆的元素保存在heap[1..hs]內   – 根在heap[1]   –K的左兒子是2k,…

細胞——求細胞數量 C++詳解

細胞——求細胞數量 C詳解 求細胞數量題目描述輸入格式輸出格式樣例樣例輸入樣例輸出 提示數據規模與約定 解法代碼 求細胞數量 題目描述 一矩形陣列由數字 0 0 0 到 9 9 9 組成&#xff0c;數字 1 1 1 到 9 9 9 代表細胞&#xff0c;細胞的定義為沿細胞數字上下左右若還…

vue3中使用component動態組件常見問題

一. 在vue3中使用動態組件問題警告處理 1. 代碼如下 <template><div v-for"(item, index) in navItems" :key"index"><component :is"item.component" :key"item.gameId"></component></div> </te…

nbcio-boot升級springboot、mybatis-plus和JSQLParser后的LocalDateTime日期json問題

升級后&#xff0c;運行顯示項目的時候出現下面錯誤 2023-08-12 10:57:39.174 [http-nio-8080-exec-3] [1;31mERROR[0;39m [36morg.jeecg.common.aspect.DictAspect:104[0;39m - json解析失敗Java 8 date/time type java.time.LocalDateTime not supported by default: add Mo…

Leetcode-每日一題【劍指 Offer 26. 樹的子結構】

題目 輸入兩棵二叉樹A和B&#xff0c;判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構) B是A的子結構&#xff0c; 即 A中有出現和B相同的結構和節點值。 例如: 給定的樹 A: 3 / \ 4 5 / \ 1 2 給定的樹 B&#xff1a; 4 / 1 返回 true&#xff0…

ffmpeg ts列表合并為mp4

操作系統&#xff1a;ubuntu 注意事項&#xff1a; 1.ts文件順序必須正確&#xff0c;也就是下一幀的dst和pst要比上一幀的大&#xff0c;否則會報錯 2.codecpar->codec_tag要設置為0&#xff0c;否則報錯Tag [27][0][0][0] incompatible with output codec id ‘27’ (avc1…

docker版jxTMS使用指南:使用jxTMS采集數據之二

本文是如何用jxTMS進行數據采集的第二部分&#xff0c;整個系列的文章請查看&#xff1a;docker版jxTMS使用指南&#xff1a;4.4版升級內容 docker版本的使用&#xff0c;請查看&#xff1a;docker版jxTMS使用指南 4.0版jxTMS的說明&#xff0c;請查看&#xff1a;4.0版升級內…

Vue + MapBox快速搭建

一、說明&#xff1a; 1.mapbox-gl自2.0版本開始不再開源&#xff0c;需要用戶在官網申請key使用。 2.maplibre GL JS是一個開源庫&#xff0c;它起源于 mapbox-gl-js 的開源分支。該庫的初始版本&#xff08;1.x&#xff09;旨在替代Mapbox的OSS版本。簡單來說maplibre是mapb…

異步場景加載詳解

異步場景加載詳解 介紹 異步場景加載是一種在Unity中加載場景的方式&#xff0c;它允許在加載過程中執行其他操作&#xff0c;并提供了加載進度的反饋。通過異步加載&#xff0c;可以避免加載大型場景時的卡頓現象&#xff0c;提高游戲的流暢性和用戶體驗。 方法 在Unity中…

C++——缺省參數

缺省參數的定義 缺省參數是聲明或定義函數時為函數的參數指定一個缺省值。在調用該函數的時候&#xff0c;如果沒有指定實參&#xff0c;則采用該形參的缺省值&#xff0c;否則使用指定的實參。 void Func(int a 0) {cout << a << endl; } int main() { Func()…

【Kubernetes】Kubernetes之Pod詳解

Pod 一、 Pod1. Pod 基礎概念2. 在 Kubrenetes 集群中 Pod 使用方式2.1 pasue 容器2.2 kubernetes 中的 pause 容器提供的功能 3. Pod 的概念和結構組成4. Pod 的分類5. Pod 容器的分類5.1 基礎容器&#xff08;infrastructure container&#xff09;5.2 初始化容器&#xff08…

07 |「異步任務」

前言 實踐是最好的學習方式&#xff0c;技術也如此。 文章目錄 前言一、進程與線程1、進程2、線程 二、實現三、異步任務加載器 一、進程與線程 1、進程 進程(Process)是操作系統分配資源的基本單位,它是一個執行中的程序實例&#xff1b;每個進程都有自己獨立的內存空間,不同…