中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

wordpress 登錄頁美化重慶seo公司怎么樣

wordpress 登錄頁美化,重慶seo公司怎么樣,it外包抽成,網(wǎng)站建站和維護計算機網(wǎng)絡 網(wǎng)絡協(xié)議初識協(xié)議分層OSI七層模型TCP/IP五層模型--初識 網(wǎng)絡中的地址管理IP地址MAC地址 網(wǎng)絡傳輸基本流程網(wǎng)絡編程套接字預備知識網(wǎng)絡字節(jié)序socket編程UDP socketTCP socket地址轉換函數(shù)Jsoncpp 進程間關系與守護進程進程組會話控制終端作業(yè)控制守護進程 網(wǎng)絡命令TC…

計算機網(wǎng)絡

  • 網(wǎng)絡協(xié)議初識
    • 協(xié)議分層
    • OSI七層模型
    • TCP/IP五層模型--初識
  • 網(wǎng)絡中的地址管理
    • IP地址
    • MAC地址
  • 網(wǎng)絡傳輸基本流程
  • 網(wǎng)絡編程套接字
    • 預備知識
    • 網(wǎng)絡字節(jié)序
    • socket編程
      • UDP socket
      • TCP socket
      • 地址轉換函數(shù)
      • Jsoncpp
  • 進程間關系與守護進程
    • 進程組
    • 會話
    • 控制終端
    • 作業(yè)控制
    • 守護進程
  • 網(wǎng)絡命令
  • TCP/IP五層模型
    • 應用層
      • HTTP協(xié)議
        • URL
        • HTTP協(xié)議格式
        • HTTP的方法
        • HTTP的狀態(tài)碼
        • HTTP的常見Header
        • HTTP cookie與HTTP session
          • HTTP cookie
          • HTTP session
      • HTTPS協(xié)議原理
    • 傳輸層
      • UDP
      • TCP
        • TCP協(xié)議端的格式
        • 確認應答機制
        • 超時重傳機制
        • 連接管理機制
          • 建立連接
          • 斷開連接
        • 滑動窗口
        • 流量控制
        • 擁塞控制
        • 延遲應答
        • 粘包問題
        • TCP全連接隊列
      • tcpdump抓包
    • 網(wǎng)絡層
      • 協(xié)議格式
      • IP報文的分片和組裝
      • 網(wǎng)段劃分
      • 網(wǎng)絡路由
    • 數(shù)據(jù)鏈路層
      • 以太網(wǎng)幀格式
      • MTU
      • ARP協(xié)議
        • ARP協(xié)議格式
        • ARP欺騙
      • 局域網(wǎng)轉發(fā)
    • 其他協(xié)議和技術
      • NAT技術
      • DNS
      • ICMP協(xié)議
      • 代理服務器
        • 正向代理
        • 反向代理
        • NAT 和代理服務器
      • 內(nèi)網(wǎng)穿透
      • 內(nèi)網(wǎng)打洞
  • 高級IO
    • 五種IO模型
    • IO多路轉接
      • select
      • poll
      • epoll

網(wǎng)絡協(xié)議初識

協(xié)議分層

使用網(wǎng)絡進行通信的一般是2臺不同的主機,伴隨著通信主機雙方物理距離的變長,勢必會帶來一系列的問題:

  • ①數(shù)據(jù)處理問題:通信雙方拿到數(shù)據(jù)后該如何處理數(shù)據(jù)
  • ②可靠性問題:如何保證數(shù)據(jù)在長距離傳輸過程中不會丟失,同時保證對方接收到的數(shù)據(jù)順序不會錯亂
  • ③主機定位問題:通信雙方如何快速定位對方主機,已將數(shù)據(jù)發(fā)送給對方
  • ④數(shù)據(jù)包局域網(wǎng)轉發(fā)問題:數(shù)據(jù)在網(wǎng)絡中是一個節(jié)點接著一個節(jié)點往下轉發(fā)最后到達目的主機的,如何將數(shù)據(jù)從一個節(jié)點轉發(fā)到下一個節(jié)點

基于以上問題,人們提出了一系列的解決方案,最后提出了網(wǎng)絡協(xié)議,該方案對網(wǎng)絡進行了分層,層與層之間是解耦合的,可以隨時替換和維護,可拓展性強,維護方便。

OSI七層模型

OSI(Open System Interconnection,開放系統(tǒng)互連)七層網(wǎng)絡模型稱為開放式系統(tǒng)互聯(lián)參考模型,是一個邏輯上的定義和規(guī)范,把網(wǎng)絡從邏輯上分為了7層,每一層都有相關對應的物理設備,比如路由器,交換機等。OSI 七層模型是一種框架性的設計方法,其只是提供了一種標準,并沒有提供最后的具體實現(xiàn),其最主要的功能使就是幫助不同類型的主機實現(xiàn)數(shù)據(jù)傳輸,它的最大優(yōu)點是將服務、接口和協(xié)議這三個概念明確地區(qū)分開來,概念清楚,理論也比較完整,通過七個層次化的結構模型使不同的系統(tǒng)不同的網(wǎng)絡之間實現(xiàn)可靠的通訊。

在這里插入圖片描述

但是,人們后來實踐起來的時候發(fā)現(xiàn)將5、6、7層分開實現(xiàn)困難,既復雜又不實用,最后人們將這三層進行了合并,統(tǒng)稱為應用層,這就是TCP/IP五層模型,由于我們很少考慮物理層,故其也稱TCP/IP四層模型。

TCP/IP五層模型–初識

在這里插入圖片描述

TCP/IP五層模型中每一層都有特定的功能,以解決前面所說的數(shù)據(jù)在網(wǎng)絡中傳輸?shù)膯栴}:

  • 物理層:負責光/電信號的傳遞方式,比如現(xiàn)在以太網(wǎng)通用的網(wǎng)線(雙絞線)、早期以太網(wǎng)采用的同軸電纜(現(xiàn)在主要用于有線電視)、光纖,現(xiàn)在的wifi無線網(wǎng)使用電磁波等都屬于物理層的概念。物理層的能力決定了最大傳輸速率、傳輸距離、抗干擾性等。集線器(Hub)工作在物理層
  • 數(shù)據(jù)鏈路層:用于解決問題④,負責設備之間的數(shù)據(jù)幀的傳送和識別。例如網(wǎng)卡設備的驅動、幀同步(即從網(wǎng)線上檢測到什么信號算作新幀的開始)、沖突檢測(如果檢測到?jīng)_突就自動重發(fā))、數(shù)據(jù)差錯校驗等工作,有以太網(wǎng)、令牌環(huán)網(wǎng)、無線LAN等標準。交換機(Switch)工作在數(shù)據(jù)鏈路層。
  • 網(wǎng)絡層:用于解決問題③,負責地址管理和路由選擇。例如在IP協(xié)議中,通過IP地址來標識一臺主機,并通過路由表的方式規(guī)劃出兩臺主機之間的數(shù)據(jù)傳輸?shù)木€路(路由)。路由器(Router)工作在網(wǎng)路層(現(xiàn)在不僅僅是在網(wǎng)絡層工作)
  • 傳輸層:用于解決問題②,負責兩臺主機之間的數(shù)據(jù)傳輸。如傳輸控制協(xié)議(TCP),能夠確保數(shù)據(jù)可靠的從源主機發(fā)送到目標主機
  • 應用層:用于解決問題①,負責應用程序間溝通,如簡單電子郵件傳輸(SMTP)、文件傳輸協(xié)議(FTP)、網(wǎng)絡遠程訪問協(xié)議(Telnet)等,我們的網(wǎng)絡編程主要就是針對應用層

一般而言,一臺主機實現(xiàn)了從傳輸層到物理層的全部內(nèi)容;對于一臺路由器,它實現(xiàn)了從網(wǎng)絡層到物理層;對于一臺交換機,它實現(xiàn)了從數(shù)據(jù)鏈路層到物理層;對于集線器,它只實現(xiàn)了物理層;但是現(xiàn)在這些并不絕對,很多交換機也實現(xiàn)了網(wǎng)絡層的轉發(fā),很多路由器也實現(xiàn)了部分傳輸層的內(nèi)容(比如端口轉發(fā))。

Linux和Windows盡管是不同的操作系統(tǒng),但他們都遵循TCP/IP協(xié)議,故二者之間可以直接進行通信(協(xié)議的本質(zhì)就是約定好的結構化的數(shù)據(jù),只要數(shù)據(jù)結構保持一致,Linux和Windows對協(xié)議的具體實現(xiàn)可以不同)。

網(wǎng)絡中的地址管理

IP地址

IP協(xié)議有兩個版本:IPv4和IPv6,我們這里只考慮IPv4。IP地址是在IP協(xié)議中,用來標識網(wǎng)絡中不同主機的地址,對于IPv4來說, IP地址是一個4字節(jié),32位的整數(shù),我們通常也使用 “點分十進制” 的字符串表示IP地址,例如 192.168.0.1 ,用點分割的每一個數(shù)字表示一個字節(jié), 每一個數(shù)字范圍是 0 - 255。

MAC地址

MAC地址用來識別數(shù)據(jù)鏈路層中相連的節(jié)點,長度為48比特位,6個字節(jié),一般用16進制數(shù)字加上冒號的形式來表示,例如:08:00:27:03:fb:19,MAC地址在網(wǎng)卡出廠時就確定了,不能修改,通常是全球唯一的(虛擬機中的mac地址不是真實的mac地址,可能會沖突,也有些網(wǎng)卡支持用戶配置mac地址)

用戶可以使用Linux指令 ifconfig 查看 MAC地址

IP 地址描述的是網(wǎng)絡中一條路徑的起點和終點,MAC 地址描述的是路途上的相鄰兩個站點之間的起點和終點。

網(wǎng)絡傳輸基本流程

不同的協(xié)議層對數(shù)據(jù)包有不同的稱謂,在應用層叫請求(request)/響應(respone),在傳輸層叫做段(segment)或數(shù)據(jù)段,在網(wǎng)絡層叫做數(shù)據(jù)報 (datagram),在鏈路層叫做幀(frame)或數(shù)據(jù)幀。應用層數(shù)據(jù)通過協(xié)議棧發(fā)到網(wǎng)絡上時,每層協(xié)議都要加上一個數(shù)據(jù)首部(header)(也稱為協(xié)議報頭),該過程稱為封裝(Encapsulation),首部信息中包含了一些類似于首部有多長、載荷(payload)有多長、上層協(xié)議是什么等信息,數(shù)據(jù)封裝成幀后發(fā)到傳輸介質(zhì)上,到達目的主機后每層協(xié)議再剝掉相應的首部,根據(jù)首部中的 “上層協(xié)議字段” 將數(shù)據(jù)交給對應的上層協(xié)議處理,該過程稱為解包與分用(解包即在當前協(xié)議層中將報頭與有效載荷分離,分用即將有效載荷交付上層協(xié)議處理)。

在這里插入圖片描述

網(wǎng)絡編程套接字

預備知識

在認識網(wǎng)絡套接字之前,我們先來學習一下端口號,現(xiàn)在我們知道可以利用ip地址將一個主機的數(shù)據(jù)發(fā)送給另一個主機,但目的主機上有那么多的進程(可以理解為有很多程序,例如既打開了微信,又打開了QQ),目的主機接收到數(shù)據(jù)后該交給哪一個進程來處理呢,我們這里使用端口號(port)來標識唯一一個進程,其是一個2字節(jié)的16位整數(shù),用來告訴操作系統(tǒng)要將該數(shù)據(jù)交給哪一個進程,通常一個進程通??梢越壎ǘ鄠€端口號,但一個端口號只能唯一綁定一個進程,因此利用ip地址和端口號就可以唯一確定計算機網(wǎng)絡中的一個進程,而在ip數(shù)據(jù)報報頭中有2個IP地址,分別叫做源ip地址、目的ip地址,用來唯一標識互聯(lián)網(wǎng)中的源主機和目的主機,在傳輸協(xié)議層的數(shù)據(jù)段報頭中有2個端口號,分別叫做源端口號和目的端口號,分別用來標識源主機的唯一進程和目的主機的唯一進程,所以網(wǎng)絡通信的本質(zhì)就是進程間通信。需要注意的是:盡管技術上可以使用進程的pid代替端口號,但這無疑會增加網(wǎng)絡的os的耦合度,因此該方法不被采用。

其中0-1023被稱為知名端口號(Well-Know Port Number),只有root用戶可以綁定,HTTP, FTP, SSH 等這些廣為使用的應用層協(xié)議,他們的端口號都是固定的,一些常用的服務器,都是用以下這些固定的端口號:

  • ssh 服務器,使用 22 端口
  • ftp 服務器,使用 21 端口
  • http 服務器,使用 80 端口
  • https 服務器,使用 443

用戶執(zhí)行以下命令就可以查看知名端口號

cat /etc/services

1024-65535是客戶端程序的端口號,就是可以由os動態(tài)為進程分配的端口號。

這里再簡單認識一些傳輸層協(xié)議:
1.TCP協(xié)議
對TCP協(xié)議(Transmission Control Protocol 傳輸控制協(xié)議)的特點如下:

  • 傳輸層協(xié)議
  • 有連接
  • 可靠傳輸
  • 面向字節(jié)流

TCP協(xié)議保證可靠性,一旦數(shù)據(jù)發(fā)送出錯就會重發(fā),意味著TCP協(xié)議更為復雜,同時其數(shù)據(jù)是面向字節(jié)流的,即目的主機可能是先接收到數(shù)據(jù)的一部分,接著再讀取到另一部分,需要自行對數(shù)據(jù)拼接,其通常用于對數(shù)據(jù)出錯丟包不容忍的場景,如網(wǎng)絡支付。

2.UDP協(xié)議
UDP協(xié)議(User Datagram Protocol 用戶數(shù)據(jù)報協(xié)議)的特點如下:

  • 傳輸層協(xié)議
  • 無連接
  • 不可靠傳輸
  • 面向數(shù)據(jù)報

UDP協(xié)議不保證可靠性,一旦數(shù)據(jù)發(fā)送出錯不做任何處理,意味著UDP協(xié)議較為簡單,同時其數(shù)據(jù)是面向數(shù)據(jù)報的,即目的主機接收到的一定是一個完整的數(shù)據(jù),其通常用于對數(shù)據(jù)丟包可以容忍的場景,如網(wǎng)絡聊天。

網(wǎng)絡字節(jié)序

我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址也有大端小端之分,網(wǎng)絡數(shù)據(jù)流同樣有大端小端之分,那么如何定義網(wǎng)絡數(shù)據(jù)流的地址呢?
發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機把從網(wǎng)絡上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存,因此,網(wǎng)絡數(shù)據(jù)流的地址應這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
TCP/IP協(xié)議規(guī)定:網(wǎng)絡數(shù)據(jù)流應采用大端字節(jié)序,即低地址高字節(jié),不管這臺主機是大端機還是小端機,都會按照這個TCP/IP規(guī)定的網(wǎng)絡字節(jié)序來發(fā)送/接收數(shù)據(jù),如果當前發(fā)送主機是小端,就需要先將數(shù)據(jù)轉成大端,否則就忽略,直接發(fā)送即可。
為使網(wǎng)絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調(diào)用以下庫函數(shù)做網(wǎng)絡字節(jié)序和主機字節(jié)序的轉換:

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

這些函數(shù)名很好記,h表示host,n表示network,l表示32位長整數(shù),s表示16位短整數(shù),例如htonl表示將32位的長整數(shù)從主機字節(jié)序轉換為網(wǎng)絡字節(jié)序,例如將IP地址轉換后準備發(fā)送。
如果主機是小端字節(jié)序,這些函數(shù)將參數(shù)做相應的大小端轉換然后返回;如果主機是大端字節(jié)序,這些函數(shù)不做轉換,將參數(shù)原封不動地返回。

socket編程

socket編程相關接口可以用于多種用途:本地通信、跨網(wǎng)絡通信、網(wǎng)絡管理等,只不過我們一般用于網(wǎng)絡通信中,其他用途可以使用其他的接口替換。

UDP socket

1.創(chuàng)建socket文件描述符:

#include <sys/types.h>         
#include <sys/socket.h>int socket(int domain, int type, int protocol);

如果成功,socket() 函數(shù)返回一個非負整數(shù),即新創(chuàng)建的套接字的文件描述符。這個描述符用于后續(xù)的網(wǎng)絡操作,如綁定地址(bind)、監(jiān)聽連接(listen,僅對 SOCK_STREAM 套接字)、接受連接(accept)、發(fā)送數(shù)據(jù)(send 或 sendto)、接收數(shù)據(jù)(recv 或 recvfrom)等。如果失敗,socket() 函數(shù)返回 -1,并設置全局變量 errno 以指示錯誤原因。參數(shù)說明:

domain:指定套接字使用的協(xié)議族。常用的協(xié)議族有 AF_INET(IPv4 地址)和 AF_INET6(IPv6 地址)。

type:指定套接字的類型。常用的類型有 SOCK_STREAM(流式套接字,如 TCP),SOCK_DGRAM(數(shù)據(jù)報套接字,如 UDP),以及 SOCK_RAW(原始套接字,允許對較低級別的協(xié)議如 IP 或 ICMP 直接訪問)。使用UDP協(xié)議時傳 SOCK_DGRAM 即可

protocol:指定使用的特定協(xié)議。大多數(shù)情況下,該參數(shù)可以設置為 0,讓系統(tǒng)自動選擇該類型套接字對應的默認協(xié)議。

無論是客戶端還是服務端,都需要先創(chuàng)建網(wǎng)絡套接字的文件描述符。

2.綁定端口號

#include <sys/types.h>     
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

成功時,bind 函數(shù)返回0。失敗時,返回-1,并設置全局變量 errno 以指示錯誤原因,參數(shù)說明:

sockfd:要綁定的套接字的文件描述符。
addr:指向特定協(xié)議的地址結構的指針,該結構包含了要綁定的IP地址和端口號。
addrlen:addr 參數(shù)的長度,通常使用 sizeof() 操作符來獲取。

sockaddr結構體說明:
sockaddr是一個通用的套接字地址結構體,用于存儲套接字的地址信息。它在 <sys/socket.h> 頭文件中定義,不過我們通常使用它的派生結構體 sockaddr_in ,在使用時可以將派生結構體指針強轉為 sockaddr 指針,我們保證這種轉換是安全的。

sockaddr_in

#include<netinet/in.h>
#include<arpa/inet.h>struct sockaddr_in {  sa_family_t sin_family;    // 地址族,對于 IPv4 來說總是 AF_INET  uint16_t    sin_port;      // 端口號,網(wǎng)絡字節(jié)序  struct in_addr sin_addr;   // IPv4 地址  // 在某些系統(tǒng)上,可能還有用于填充的字節(jié),以確保結構體大小與 sockaddr 一致  // 但這通常不是顯式聲明的字段  
};  // 其中,in_addr 結構體通常定義如下:  
struct in_addr {  uint32_t s_addr; // IPv4 地址,網(wǎng)絡字節(jié)序  
};

sockaddr_in 是網(wǎng)絡編程中用于表示 IPv4 套接字地址的結構體。它是在 <netinet/in.h> 或 <arpa/inet.h>(取決于操作系統(tǒng)和編譯器)頭文件中定義的,是 sockaddr 結構體的一個派生或特定于協(xié)議的版本。sockaddr_in 結構體提供了存儲 IPv4 地址、端口號以及地址族(對于 IPv4 來說,地址族總是 AF_INET)的字段。
需要注意在填充 sockaddr_in 結構體時需要將主機字節(jié)序轉為網(wǎng)絡字節(jié)序,同時也不推薦綁定任何一個確定的IP地址(一臺機器可以擁有多個ip地址),而是使用任意綁定,即將s_addr字段填充0或INADDR_ANY(宏值為0),相當于綁定了0.0.0.0,表示任意綁定。如果我們綁定的ip地址是127.0.0.1,則表示本地環(huán)回,即數(shù)據(jù)只是走一遍網(wǎng)絡協(xié)議棧,并不發(fā)送出去,通常用于代碼測試。
為了安全起見,可以在定義 sockaddr_in結構體后使用 bzero() 將該結構體置0:

#include <string.h>
void bzero(void *s, size_t n);

sockaddr 還有其他的派生結構體,如 sockaddr_in6 用于處理 IPv6 地址和端口號,sockaddr_un用于本地通信。

服務端需要調(diào)用bind()顯示綁定,但客戶端不需要這樣顯示綁定(注意是不需要用戶去手動顯示綁定,而不是不需要綁定,os在底層會在客戶端首次發(fā)送數(shù)據(jù)時自動幫我們進行綁定),盡管用戶也可以選擇顯示綁定,但我們不建議這么做,因為可能會出現(xiàn)多個進程顯示綁定同一個端口號的情況,就會導致只有一個進程可以綁定端口號,從而導致異常。
如果用戶用的是自己購買的云服務器進行socket編程,需要用戶自己到購買的云服務器開通端口號才可以對服務器的端口號進行綁定。

3.讀寫數(shù)據(jù)

#include <sys/types.h>
#include <sys/socket.h>
//向套接字寫數(shù)據(jù)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
//從套接字讀數(shù)據(jù)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);		
  • ①sendto()

成功時,sendto 返回發(fā)送的字節(jié)數(shù),失敗時,返回-1,并設置全局變量 errno :以指示錯誤原因。
sockfd :是已打開的套接字描述符。
buf :指向要發(fā)送數(shù)據(jù)的緩沖區(qū)。
len :是要發(fā)送數(shù)據(jù)的長度。
flags :通常設置為0,但可以用于控制發(fā)送操作的行為(如設置消息邊界)。
dest_addr :是一個指向 sockaddr 結構體的指針,包含了目標地址和端口信息。
addrlen :是 dest_addr 的長度。

  • ②recvfrom()

成功時,recvfrom 返回實際接收到的字節(jié)數(shù),失敗時,返回-1,并設置全局變量 errno 以指示錯誤原因。
sockfd :是接收數(shù)據(jù)的套接字描述符。
buf :是指向存儲接收數(shù)據(jù)的緩沖區(qū)的指針。
len :是緩沖區(qū)的大小,即最多可以接收多少字節(jié)的數(shù)據(jù)。
flags :通常設置為0,但可以用于控制接收操作的行為(如設置消息邊界標志等)。
src_addr :是一個指向 sockaddr 結構體的指針,用于存儲發(fā)送方的地址信息。調(diào)用前,這個結構體可以是未初始化的,調(diào)用后,它將被填充為發(fā)送方的地址信息。
addrlen :是一個指向整數(shù)的指針,它包含了src_addr結構體的大小。在調(diào)用之前,應該設置為這個結構體的大小(如sizeof(struct sockaddr_in)對于IPv4),函數(shù)返回時,它可能包含了實際存儲在src_addr中的地址信息的大小。

TCP socket

1.創(chuàng)建socket文件描述符:

#include <sys/types.h>         
#include <sys/socket.h>int socket(int domain, int type, int protocol);

用于打開一個網(wǎng)絡通訊端口,成功返回一個文件描述符,失敗返回-1,參數(shù)說明:
domain :指定套接字使用的協(xié)議族。常用的協(xié)議族有 AF_INET(IPv4 地址)和 AF_INET6(IPv6 地址)。

type :指定套接字的類型。對于TCP協(xié)議傳 SOCK_STREAM 即可

protocol :指定使用的特定協(xié)議。大多數(shù)情況下,該參數(shù)可以設置為 0,讓系統(tǒng)自動選擇該類型套接字對應的默認協(xié)議。

2.服務端設置地址復用

在舊的服務器進程終止后(也可能是因為BUG導致服務器意外終止),需要等待一段時間才能重新綁定相同的地址和端口號,原因與TCP協(xié)議的設計有關,特別是與TCP連接的終止過程和“TIME_WAIT”狀態(tài)有關。地址復用(Address Reuse)是指在網(wǎng)絡編程中,允許在同一臺主機上的多個套接字(Socket)綁定到同一個網(wǎng)絡地址和端口。在TCP/IP網(wǎng)絡中,通常情況下,一個端口在同一時間只能被一個套接字綁定。地址復用技術允許在特定條件下違反這一規(guī)則,這樣在服務器終止后,其可以快速重啟保持之前的端口號。

#include<sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const voi*optval, socklen_t optlen);

setsockopt函數(shù)是一個用于設置套接字選項的函數(shù),如果設置套接字選項成功,setsockopt函數(shù)返回0。這表示指定的套接字選項已經(jīng)被成功設置,如果設置套接字選項失敗,setsockopt函數(shù)返回-1,參數(shù)說明:
sockfd:套接字文件描述符,用于標識要設置選項的套接字。
level:指定選項的級別,它決定了optname的解釋。常見的級別包括SOL_SOCKET(套接字級別選項)、IPPROTO_TCP(TCP協(xié)議級別選項)等。
optname:指定要設置的選項的名稱,如緩沖區(qū)大小、超時設置、廣播選項等,可以通過設置 SO_REUSEADDR 套接字選項來允許地址復用。
optval:一個指向存儲選項值的緩沖區(qū)的指針,該值將被設置到套接字上。
optlen:optval緩沖區(qū)的長度,用于確保傳遞了正確的數(shù)據(jù)大小。

使用示例:

int sockfd;  
int opt = 1; //用于將SO_REUSEADDR設置為1, 如果要設置地址復用,必須將SO_REUSEADDR 設置為1// 創(chuàng)建套接字(省略了錯誤檢查和細節(jié))  
sockfd = socket(AF_INET, SOCK_STREAM, 0);  
if (sockfd < 0) {  perror("socket creation failed");  exit(EXIT_FAILURE);  
}  // 設置SO_REUSEADDR選項  
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {  perror("setsockopt");  close(sockfd); // 出錯時關閉套接字  exit(EXIT_FAILURE);  
}  

3.綁定端口號:

#include <sys/types.h>     
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

與UDP的綁定用法一樣,其注意事項也與UDP的綁定一樣

4.服務端開啟監(jiān)聽:

#include <sys/types.h>
#include <sys/socket.h>int listen(int sockfd, int backlog);

讓sockfd處于監(jiān)聽狀態(tài),用于服務端等待客戶端connect()鏈接服務端,成功返回0,失敗返回-1,參數(shù)說明:
sockfd:指向一個已打開的套接字,用于進行監(jiān)聽
backlog:用于定義內(nèi)核應該為相應套接字排隊的最大連接數(shù)。即當服務器繁忙時,新的連接請求會被放入隊列中,直到服務器準備好接受連接(即調(diào)用 accept 函數(shù))。

5.客戶端發(fā)起連接:

#include <sys/types.h>       
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

用于客戶端向服務器發(fā)起連接,以建立客戶端與服務器之間的連接,成功返回0,失敗返回-1,參數(shù)說明:
sockfd :這是客戶端套接字的文件描述符,它標識了一個打開的套接字。這個套接字之前應該通過 socket 函數(shù)創(chuàng)建,但尚未與任何服務器建立連接。
addr :這是一個指向 sockaddr 結構體的指針,該結構體包含了服務器的地址信息。由于 sockaddr 是一個通用結構體,實際使用時通常會使用 sockaddr_in(對于 IPv4 地址)或 sockaddr_in6(對于 IPv6 地址)等更具體的結構體,并通過類型轉換后傳遞給 connect 函數(shù),具體可以參考前面UDP socket對struct sockaddr的介紹。
addrlen:這個參數(shù)指定了 addr 指向的地址結構體的長度。對于 sockaddr_in 來說,這個長度通常是 sizeof(struct sockaddr_in)

6.服務端獲取鏈接:

#include <sys/types.h>
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

用于服務端獲取客戶端使用connect()發(fā)起的連接,成功返回一個文件描述符,用于數(shù)據(jù)讀寫,失敗返回-1并設置對應錯誤碼,參數(shù)說明:
sockfd:這是監(jiān)聽套接字的文件描述符
addr :這是一個輸出型參數(shù),用于接收客戶端的地址信息。如果調(diào)用者對此信息不感興趣,可以將其設置為 NULL。如果提供了非 NULL 的地址,accept 函數(shù)會將客戶端的地址填充到該結構體中。
addrlen:這是一個指向 socklen_t 變量的指針,該變量在調(diào)用 accept 之前應該被設置為 addr 指向的地址結構體的長度。在 accept 返回時,該變量會被更新為實際接收到的地址長度。

7.數(shù)據(jù)讀寫:

#include <unistd.h>
//向套接字寫數(shù)據(jù)
ssize_t write(int fd, const void *buf, size_t count);
//從套接字讀數(shù)據(jù),但無法得到發(fā)送方的信息
ssize_t read(int fd, void *buf, size_t count);

需要注意的是在TCP中并不是直接利用sockfd進行消息讀寫的,而是對accept()獲取連接成功后的文件描述符進行讀寫,同時當客戶端斷開連接后,服務端需要使用 close() 關閉該文件描述符,以防止文件描述符泄漏。

地址復用:

地址轉換函數(shù)

在IPv4中IP地址有2中表示方法:32位整型表示的IP地址和點分十進制風格的IP地址,我們通常需要將IP地址在二者之間進行相互轉換,下面介紹一些轉換函數(shù):
1.字符串風格轉整型風格:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);

用于將點分十進制的 IPv4 地址字符串(如 “192.168.1.1”)轉換為 網(wǎng)絡字節(jié)順序 的整數(shù)(in_addr_t 類型)的函數(shù),如果輸入字符串是一個有效的 IPv4 地址,則 inet_addr 函數(shù)返回該地址的網(wǎng)絡字節(jié)順序表示(in_addr_t 類型)。如果輸入字符串不是一個有效的 IPv4 地址,或者包含無法識別的字符,則函數(shù)返回 INADDR_NONE(通常是 0xFFFFFFFF),表示轉換失敗,參數(shù)說明:
cp:指向一個字符串,該字符串包含了需要轉換的 IPv4 地址

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);

用于將點分十進制的 IPv4 地址字符串(如 “192.168.1.1”)轉換成網(wǎng)絡字節(jié)順序的整型形式,并存儲在 struct in_addr 結構體中,如果輸入字符串是一個有效的 IPv4 地址,則 inet_aton 函數(shù)返回非零值(通常為 1),如果輸入字符串不是一個有效的 IPv4 地址,則函數(shù)返回零,參數(shù)說明:
cp:指向一個字符串,該字符串包含了需要轉換的 IPv4 地址
inp:指向 struct in_addr 結構體的指針,該結構體用于存儲轉換后的二進制形式的 IP 地址。struct in_addr 通常包含一個無符號 32 位整數(shù)(uint32_t),用于表示網(wǎng)絡字節(jié)順序的 IP 地址

struct in_addr 的具體實現(xiàn)可能因操作系統(tǒng)而異,但最常見的定義如下所示:

struct in_addr 
{  uint32_t s_addr;  // 這是一個無符號 32 位整數(shù),用于存儲 IP 地址  
};

#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);

用于將點分十進制(對于 IPv4)或十六進制字符串(對于 IPv6)的 IP 地址轉換為 網(wǎng)絡字節(jié)順序 的二進制形式的函數(shù)。這個函數(shù)比 inet_addr 函數(shù)更加靈活和強大,因為它同時支持 IPv4 和 IPv6 地址,并且提供了更明確的錯誤處理機制。如果轉換成功,函數(shù)返回 1;如果輸入的字符串不是一個有效的 IP 地址(對于指定的地址族),函數(shù)返回 0;如果發(fā)生錯誤(例如,無效的地址族),函數(shù)返回 -1,并設置 errno 以指示錯誤原因。參數(shù)說明:

af:地址族(Address Family),指定要轉換的地址類型。對于 IPv4,這個值應該是 AF_INET;對于 IPv6,這個值應該是 AF_INET6。
src:指向一個字符串,該字符串包含了需要轉換的 IP 地址(例如,對于 IPv4 是 “192.168.1.1”,對于 IPv6 是 “2001:0db8:85a3:0000:0000:8a2e:0370:7334”)。
dst:指向用于存儲轉換后結果的緩沖區(qū)的指針。對于 IPv4 地址,這個緩沖區(qū)應該足夠大以存儲一個 struct in_addr(或等效的 uint32_t)的值;對于 IPv6 地址,這個緩沖區(qū)應該足夠大以存儲一個 struct in6_addr 的值。

2.整型風格轉字符串風格:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);

用于將網(wǎng)絡字節(jié)順序的 IPv4 地址(存儲在 struct in_addr 結構體中)轉換為點分十進制的字符串形式的函數(shù)。函數(shù)返回一個指向靜態(tài)分配的字符數(shù)組的指針,該數(shù)組包含了輸入地址的點分十進制表示。需要注意的是,這個返回的指針指向的是一個靜態(tài)分配的內(nèi)存區(qū)域,這意味著每次調(diào)用 inet_ntoa 時,它都會覆蓋上一次調(diào)用的結果。參數(shù)說明:
in: 是一個 struct in_addr 類型的結構體,它包含了一個網(wǎng)絡字節(jié)順序的 IPv4 地址。

需要注意的是inet_ntoa() 函數(shù)并不是線程安全的,因為它使用了靜態(tài)分配的緩沖區(qū)來存儲結果字符串,該靜態(tài)緩沖區(qū)不需要用戶去釋放。在多線程環(huán)境中,建議使用 inet_ntop() 函數(shù)作為替代,該函數(shù)提供了更多的靈活性和線程安全性。

#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

用于將網(wǎng)絡地址(IPv4 或 IPv6)從網(wǎng)絡字節(jié)順序轉換為點分十進制(對于 IPv4)或十六進制字符串(對于 IPv6)的函數(shù)。這個函數(shù)比 inet_ntoa 更靈活且安全,因為它允許你指定一個緩沖區(qū)來存儲轉換后的字符串,從而避免了使用靜態(tài)內(nèi)存的問題,這在多線程環(huán)境中尤為重要。返回一個指向 dst 的指針,如果轉換失敗,則返回 NULL,并設置 errno 以指示錯誤原因。參數(shù)說明:
af:地址族,AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。
src:指向包含網(wǎng)絡字節(jié)順序地址的緩沖區(qū)的指針。對于 IPv4,它是一個指向 struct in_addr 的指針;對于 IPv6,它是一個指向 struct in6_addr 的指針。
dst:指向用于存儲轉換后字符串的緩沖區(qū)的指針。
size:dst 緩沖區(qū)的大小(以字節(jié)為單位)。

這些函數(shù)也比較好記,a表示地址(address),即點分十進制形式的IP地址;n表示網(wǎng)絡(network),即網(wǎng)絡字節(jié)序的IP地址;p譯為表示(presentation),即人類可讀的表示法,可用于點分十進制的 IPv4 地址或冒號分隔的 IPv6 地址
同時需要注意前面這些所有函數(shù)(包括字符串轉整型)所用到的或返回的整型風格的IP地址都是網(wǎng)絡字節(jié)序的。

Jsoncpp

由于Tcp協(xié)議是面向字節(jié)流的,不保證接收方能一次接收到一個完整的數(shù)據(jù)報,因此需要用戶在應用層將數(shù)據(jù)的格式進行約定(即自定義協(xié)議或者私有協(xié)議),對數(shù)據(jù)進行包裝后再將數(shù)據(jù)發(fā)送給對方,對方接收到數(shù)據(jù)化再按照約定對數(shù)據(jù)進行解析,解析出一個完整的報文后才交付上層讓上層對數(shù)據(jù)進行處理。考慮到系統(tǒng)復雜性和維護成本,同時為了服務的拓展性,一般發(fā)送方會將數(shù)據(jù)jason化,將數(shù)據(jù)結構或對象轉換為一種格式,以便在網(wǎng)絡上傳輸或存儲到文件中,再將數(shù)據(jù)按照自定義協(xié)議進行包裝通過tcp協(xié)議發(fā)送到網(wǎng)絡。
Jsoncpp 是一個用于處理 JSON 數(shù)據(jù)的 C++ 庫。它提供了將 JSON 數(shù)據(jù)序列化為字符串以及從字符串反序列化為 C++ 數(shù)據(jù)結構的功能。Jsoncpp 是開源的,廣泛用于各種需要處理 JSON 數(shù)據(jù)的 C++ 項目中,其安裝方法如下:

ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

在編譯時需要添加選項 -ljsoncpp

1.序列化
①使用 Json::Value 的 toStyledString 方法:

#include <jsoncpp/json/json.h>Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();

該方法使用簡單,將 Json::Value 對象直接轉換為格式化的 JSON 字符串

②. 使用 Json::StreamWriter:

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter 的工廠std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}

該方法提供了更多的定制選項,如縮進、換行符等

③使用 Json::FastWriter:

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}

該方法比 StyledWriter 更快,因為它不添加額外的空格和換行符。

2.反序列化
①使用 Json::Reader:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>Json::Reader reader;
Json::Value root;// 從字符串s中讀取 JSON 數(shù)據(jù)
if (reader.parse(s,root)) 
{
// 解析成功,訪問 JSON 數(shù)據(jù)std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();
}

該方法提供詳細的錯誤信息和位置,方便調(diào)試。

②使用 Json::CharReader 的派生類:

  • 在某些情況下,可能需要更精細地控制解析過程,可以直接使用Json::CharReader 的派生類。
  • 但通常情況下,使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足夠了。

進程間關系與守護進程

進程組

每一個進程除了有一個進程 ID(PID)之外還屬于一個進程組。進程組是一個或者多個進程的集合,一個進程組可以包含多個進程。 每一個進程組也有一個唯一的進程組 ID(PGID),并且這個 PGID 類似于進程 ID,同樣是一個正整數(shù),可以存放在 pid_t 數(shù)據(jù)類型中。通常由一個進程組之間的進程相互配合完成某個特定的任務。bash進程是一個獨立的進程組,當用戶新建一個會話時,該會話也會有一個自己的-bash進程,也是自成一個獨立的進程組。
可以使用指令 ps -eo pid,pgid,ppid,comm 查看進程的相關信息,包括進程id,進程組id等信息,其中:

  • e 選項表示 every 的意思,表示輸出每一個進程信息
  • o 選項以逗號操作符(,)作為定界符,可以指定要輸出的列

每一個進程組都有一個組長進程,進程組組長可以創(chuàng)建一個進程組或者創(chuàng)建該組中的進程,而進程組的生命周期為從進程組創(chuàng)建開始到其中最后一個進程離開為止,即只要某個進程組中有一個進程存在,則該進程組就存在,這與其組長進程是否已經(jīng)終止無關。

會話

會話其實和進程組息息相關,當我們打開shell連接遠端服務器時,shell會為我們打開一個終端文件,然后新啟動一個-bash進程,這就相當于建立了一個會話,會話可以看成是一個或多個進程組的集合,一個會話可以包含多個進程組,其內(nèi)部至少有一個進程即 -bash 進程,每一個會話也有一個會話 ID(SID),一般為-bash進程的id。
在這里插入圖片描述
在該會話下運行一個可執(zhí)行程序時,該可執(zhí)行程序就會變成一個與該會話相關聯(lián)的進程,如果我們想另起一個新會話,讓當前會話下的可執(zhí)行程序稱為新會話下的進程,從而與當前會話取關聯(lián),可以使用函數(shù):

#include <unistd.h>
pid_t setsid(void);

創(chuàng)建會話成功返回 SID,失敗返回-1
調(diào)用該函數(shù)的進程會變成新會話的會話首進程,此時新會話中只有唯一的一個進程,而且進程會變成進程組組長,新進程組 ID 就是當前調(diào)用進程 ID,該進程沒有控制終端,如果在調(diào)用 setsid 之前該進程存在控制終端,則調(diào)用之后會切斷聯(lián)系。
需要注意的是:這個接口如果調(diào)用進程原來是進程組組長,則會報錯,為了避免這種情況,我們通常的使用方法是先調(diào)用 fork 創(chuàng)建子進程,父進程終止,子進程繼續(xù)執(zhí)行,因為子進程會繼承父進程的進程組 ID,而進程 ID 則是新分配的,就不會出現(xiàn)錯誤的情況。

控制終端

在 UNIX 系統(tǒng)中,用戶通過終端登錄系統(tǒng)后得到一個 Shell 進程,這個終端成為 Shell進程的控制終端??刂平K端是保存在 PCB 中的信息,我們知道 fork 進程會復制 PCB中的信息,因此由 Shell 進程啟動的其它進程的控制終端也是這個終端。默認情況下沒有重定向,每個進程的標準輸入、標準輸出和標準錯誤都指向控制終端,進程從標準輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫也就是輸出到顯示器上。
每一個終端都是一個文件,他們在目錄 ==/dev/pts/==下,如/dev/pts/0和/dev/pts/1都是一個終端文件,可以像普通文件一樣讀寫和重定向。
在同一個會話中,任何時候都只允許運行一個前臺進程組,但可以同時運行多個后臺進程組,而只有前臺進程組才可以與控制終端進行交互,Linux中的命令就是通過創(chuàng)建一個前臺進程去執(zhí)行,而bash進程轉為后臺進程了,此時該命令執(zhí)行期間,我們再次向終端輸入命令是不會有響應的,因為只有前臺進程可以與終端交互,bash進程此時已經(jīng)轉為后臺進程了沒有接收到任何輸入,我們可以在命令后面加上&,表示在執(zhí)行該命令的進程直接轉為后臺進程。

作業(yè)控制

作業(yè)是針對用戶來講的,用戶完成某項任務會啟動一些進程,一個作業(yè)既可以只包含一個進程,也可以包含多個進程,進程之間互相協(xié)作完成任務。
Shell 分前后臺來控制作業(yè)或者進程組。一個前臺作業(yè)可以由多個進程組成,一個后臺作業(yè)也可以由多個進程組成,Shell 可以同時運?一個前臺作業(yè)和任意多個后臺作業(yè),這稱為作業(yè)控制。
例如下列命令就是一個作業(yè),它包括兩個命令,在執(zhí)?時 Shell 將在前臺啟動由兩個進程組成的作業(yè):

cat /etc/filesystems | head -n 5

放在后臺執(zhí)?的程序或命令稱為后臺命令,可以在命令的后面加上&符號從而讓Shell 識別這是一個后臺命令,后臺進程執(zhí)行完后會返回一個作業(yè)號以及一個進程號(PID)。
可以使用指令 jobs 查看后臺作業(yè),選項-l 則顯示作業(yè)的詳細信息,選項-p 則只顯示作業(yè)的 PID。以下是返回的一些符號和狀態(tài)解釋:
+: 表示該作業(yè)號是默認作業(yè)
-:表示該作業(yè)即將成為默認作業(yè)
無符號: 表示其他作業(yè)
對于一個用戶來說,只能有一個默認作業(yè)(+),同時也只能有一
個即將成為默認作業(yè)的作業(yè)(-),當默認作業(yè)退出后,該作業(yè)會成為默認作業(yè)。
作業(yè)狀態(tài):
在這里插入圖片描述
可以使用指令 fg 作業(yè)號 將該作業(yè)轉為后臺作業(yè),如果想將一個作業(yè)轉為前臺作業(yè),先使用ctrl+z將當前作業(yè)暫停,其就會自動轉為后臺作業(yè),接著使用指令 bg 作業(yè)號 啟動該作業(yè)。

守護進程

當我們開發(fā)了一個服務端后,我們是在一個會話下面啟動運行該服務端的,如果該會話退出,會話內(nèi)的任務也會退出,因此我們就需要為該服務端進行新建一個會話,該會話只有服務端這一個進程,此時該進程就會稱為一個守護進程(也稱為精靈進程,本質(zhì)就是一個孤兒進程),該進程獨立運行,通常不與任何用戶交互,這樣就保證了服務端運行時的穩(wěn)定性。我們將一個進程變?yōu)槭刈o進程的一般步驟為:

  1. 忽略可能引起程序異常退出的信號
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
  1. 讓自己不要成為組長
if (fork() > 0)
exit(0);
  1. 設置讓自己成為一個新的會話, 后面的代碼其實是子進程在走
setsid();
  1. 每一個進程都有自己的 CWD,可以選擇將當前進程的 CWD 更改成為 /根目錄
chdir(目錄);
  1. 已經(jīng)變成守護進程啦,不需要和用戶的輸入輸出,錯誤進行關聯(lián)了,可以直接關閉對應文件描述符
close(0);
close(1);
close(2);

還可以選擇重定向到文件dev_null中

int fd = open(dev_null, O_RDWR);
if (fd > 0)
{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);
}

輸入到文件dev_null的數(shù)據(jù)會被忽略,從該文件讀取數(shù)據(jù)會讀到空,如果需要,推薦使用重定向這種方法。

網(wǎng)絡命令

1. netstat

netstat(Network Status,網(wǎng)絡狀態(tài))是一個用來查看網(wǎng)絡狀態(tài)的重要工具,用法為:

netstat [選項]

常用選項:

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

2. pidof

用于通過進程名查看進程id,用法為:

pidof [選項] [進程名稱]
  • -s:僅返回一個進程號。如果有多個與指定名稱匹配的進程在運行,pidof將只返回第一個找到的進程的PID。
  • -c:僅顯示具有相同“root”目錄的進程。這個選項只對root用戶有效,它可以幫助用戶篩選出具有特定根目錄的進程。
  • -x:顯示由腳本開啟的進程。這個選項允許用戶查找由shell腳本或其他程序啟動的進程。
  • -o:指定不顯示的進程ID。用戶可以通過這個選項來排除某些特定的進程ID,以避免它們在輸出中出現(xiàn)

3. ping

ping(Packet Internet Groper,網(wǎng)絡包探測器)是一個程序,用于驗證網(wǎng)絡的連通性,同時也會統(tǒng)計響應時間和 TTL(IP 包中的Time To Live,生存周期),ping 命令基于 ICMP, 是在網(wǎng)絡層,使用時ping 命令會先發(fā)送一個 ICMP Echo Request 給對端,對端接收到之后會返回一個 ICMP Echo Reply,根本不關心端口號,用法為:

ping [選項] [目標主機的IP地址或域名]

常用選項有:

  • -t:持續(xù)發(fā)送ping請求,直到手動停止。

  • -n:指定發(fā)送的ping請求次數(shù)。默認情況下,ping命令會發(fā)送4個請求。例如,“ping -n 10 192.168.1.1”將發(fā)送10個數(shù)據(jù)包進行測試。

  • -l(或-s,具體取決于操作系統(tǒng)):設置發(fā)送的數(shù)據(jù)包大小。默認情況下,ping命令發(fā)送的數(shù)據(jù)包大小為32字節(jié)。通過指定大小,可以測試網(wǎng)絡在不同數(shù)據(jù)包大小下的性能。

  • -a:將IP地址解析為主機名。當目標主機的IP地址不容易記憶時,可以使用這個參數(shù)將IP地址轉換為更易于理解的主機名。例如,“ping -a 192.168.1.1”可能會返回類似“Pinging example-PC [192.168.1.1]”的信息。

  • -w:設置超時時間(某些操作系統(tǒng)中可能不支持此參數(shù))。Ping命令默認的超時時間是一定的毫秒數(shù)(如400毫秒)。通過指定超時時間,可以更靈活地控制ping命令的等待響應時間。例如,“ping -w 1000 192.168.1.1”將超時時間設置為1000毫秒

4. traceroute

traceroute(Trace Route,路由追蹤),用于顯示數(shù)據(jù)包從源主機到目的主機所經(jīng)過的路由器路徑,它也是基于ICMP協(xié)議實現(xiàn)的,不關心端口號,用法為:

traceroute [目標主機IP地址或域名]

常用選項有:

  • -n:禁用域名解析,只顯示IP地址,這可以加快命令的執(zhí)行速度,特別是在DNS解析較慢或不可用時。
  • -m <跳數(shù)>:設置檢測數(shù)據(jù)包的最大存活數(shù)值TTL的大小,即限制追蹤的最大跳數(shù)。
  • -w <秒數(shù)>:設置等待每個路由器響應的超時時間。如果路由器在指定時間內(nèi)沒有響應,則顯示超時消息。
  • -q <次數(shù)>:指定每個跳數(shù)發(fā)送的數(shù)據(jù)包數(shù)量。默認情況下,Traceroute會發(fā)送多個數(shù)據(jù)包以獲得更準確的延遲統(tǒng)計信息。
  • -I(僅Unix/Linux):使用ICMP Echo請求代替UDP數(shù)據(jù)包進行追蹤。這在某些情況下可能更有用,特別是當目標主機對UDP數(shù)據(jù)包有特定過濾規(guī)則時。

5. telnet

Telnet(TErminaL NETwork)是一種基于文本的遠程登錄協(xié)議,允許用戶通過網(wǎng)絡連接到遠程計算機,并在遠程計算機上執(zhí)行命令,用法為:

telnet [IP地址] [端口號]

它默認使用端口號是23

6.ssh
SSH(Secure Shell)命令是一種用于遠程登錄、文件傳輸及遠程命令執(zhí)行的強大工具,它通過網(wǎng)絡在客戶端和服務器之間建立安全的加密連接,用法為:

ssh [選項] 用戶名@主機名或IP地址

默認使用的端口號是22

7.ARP
ARP命令是用于操作和管理計算機網(wǎng)絡中的ARP(Address Resolution Protocol,地址解析協(xié)議)緩存的命令。ARP協(xié)議的主要功能是將網(wǎng)絡中的IP地址解析為目標硬件地址(MAC地址),以保證通信的順利進行,用法為:

arp [選項] [參數(shù)]

常用選項和參數(shù):

  • -a:顯示所有接口的當前ARP緩存表,將列出計算機中存儲的已解析IP地址和相應的MAC地址。
  • -d InetAddr [IfaceAddr]:刪除指定IP地址的ARP緩存項。InetAddr代表要刪除的IP地址,IfaceAddr代表指派給該接口的IP地址(可選)。如果未指定IfaceAddr,則使用第一個適用的接口。
  • -s InetAddr EtherAddr [IfaceAddr]:向ARP緩存添加靜態(tài)項。InetAddr代表IP地址,EtherAddr代表物理地址(MAC地址),IfaceAddr代表指派給該接口的IP地址(可選)。通過此命令添加的項屬于靜態(tài)項,它們不會因ARP緩存超時而被刪除。
  • -g:與-a相同,用于顯示本地ARP緩存表。在某些操作系統(tǒng)中,可以使用-g選項命令來顯示ARP緩存表。
  • -n:顯示本地ARP緩存表,但不解析主機名。這個命令可以顯示ARP緩存表,但不會嘗試將IP地址解析為主機名。

TCP/IP五層模型

應用層

開發(fā)者寫的一個個用于解決實際問題的網(wǎng)絡程序都是在應用層進行開發(fā),由于其下一層傳輸層可能是基于TCP也可能是基于UDP,因此應用層對不同的傳輸層協(xié)議有不同的處理方法:對傳輸層協(xié)議為UDP時,由于UDP是面向數(shù)據(jù)報的,開發(fā)者不需要在應用層添加自定義協(xié)議保證接收到一個完整的數(shù)據(jù)報,但為了維護方便,通常會在應用層對數(shù)據(jù)進行json序列化和反序列化;對傳輸層協(xié)議為TCP,由于TCP是面向字節(jié)流的,不保證接收方能一次就接收到一個完整的數(shù)據(jù)報,因此需要開發(fā)者在應用層添加自定義協(xié)議保證接收到一個完整的數(shù)據(jù)報再進行處理,為了維護方便,其也要在應用層對數(shù)據(jù)進行json序列化和反序列化。

HTTP協(xié)議

我們經(jīng)常說應用層的協(xié)議是由開發(fā)者自己定制的,其實前人已經(jīng)定制好了一些現(xiàn)成的且非常好用的協(xié)議,可以直接供我們參考使用,其中比較常用的就是HTTP協(xié)議,即超文本傳輸協(xié)議(https與http是類似的,只不過https會對文本進行加密處理),其是基于TCP的應用層協(xié)議。

URL

我們平時所說的網(wǎng)址,其實就是URL(Uniform Resource Locator,統(tǒng)一資源定位符)
在這里插入圖片描述
片段標識符不是發(fā)送給服務器的,而是由瀏覽器解析并在客戶端內(nèi)部使用,以便直接定位到頁面內(nèi)的某個特定元素或執(zhí)行與錨點相關聯(lián)的JavaScript代碼,可以忽略。
總之,除了服務器地址和目標文件外,其余的都可以省略,在HTML中,服務器地址也可以省略,表示請求服務器ip地址與當前HTML所屬ip地址一致。
省略協(xié)議方案名,則默認使用的協(xié)議是HTTP協(xié)議
省略端口號,瀏覽器默認根據(jù)協(xié)議選擇端口號,HTTP協(xié)議默認訪問的服務器端口號為80,HTTPS協(xié)議默認訪問的服務器端口號為443。
省略文件路徑,但目標文件名不能省,表示請求的文件路徑為‘/’,服務端需要自己解析‘\’表示那個文件路徑

像/?:等這樣的字符,已經(jīng)被url當做特殊意義理解了(在URL中?表示接下來是參數(shù),參數(shù)之間以&分隔,如果后面還有其他內(nèi)容則用#分隔表示參數(shù)部分結束),因此這些字符不能隨意出現(xiàn),如果某個參數(shù)中需要帶有這些特殊字符,就必須先對特殊字符進行轉義,轉義的規(guī)則如下:
將需要轉碼的字符轉為16進制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式,例如+的ASCII碼是43,被轉義成 %2B。

HTTP協(xié)議格式

1.HTTP協(xié)議請求格式
[方法]+[空格]+[URL]+[空格]+[版本號]+[換行符]+[請求屬性]+[換行符]+[空行(其實就是換行符)]+[請求正文]
在這里插入圖片描述

其中版本號可以忽略不寫,請求屬性也稱為Header,是用冒號分割的鍵值對,如果有多組屬性,也需要使用換行符進行分割,空行是用于表示Header部分結束,如果沒有在URL中指定請求資源的文件路徑,默認請求資源路徑是:“/”,此時服務端如果解析到請求路徑是:“/”,服務端開發(fā)者可以自定義返回一個主網(wǎng)頁供客戶端使用,正文部分也稱為Body,Body允許為空字符串,如果Body存在,則要在Header中加一個Content-Length屬性來標識Body的長度。
例如以下一個http請求:

POST http//example.edu.cn/main.html HTTP/1.1
Host: example.edu.cn
Content-Length: 15

example_request

2.HTTP協(xié)議應答格式
[版本號]+[空格]+[狀態(tài)碼]+[空格]+[狀態(tài)碼解釋]+[換行符]+[請求的屬性]+[換行符]+[空行]+[應答正文]
在這里插入圖片描述
正文部分也稱為Body,Body允許為空字符串,如果Body存在,則要在Header中加一個Content-Length屬性來標識Body的長度,如果服務器返回了一個html頁面(HTML是一種用于創(chuàng)建網(wǎng)頁內(nèi)容的標記語言),那么html頁面內(nèi)容就是在body中,瀏覽器獲取html內(nèi)容后會根據(jù)獲得的內(nèi)容構建渲染網(wǎng)頁呈現(xiàn)給用戶,關于HTML的學習和使用可以參考:form表單 或 HTML

例如以下的一個http響應:

HTTP/1.1 200 OK
Content-language: zh-CN
Content-Length: 16example_response
HTTP的方法

在這里插入圖片描述
1.GET方法:
用于請求URL指定的資源,也可以以URL的形式向服務端提交參數(shù),即在URL中?表示接下來是參數(shù),參數(shù)之間以&分隔,使用這種方法時但不建議上傳的參數(shù)太大。

2.POST方法:
用于傳輸實體的主體,通常用于提交表單數(shù)據(jù),可以在正文中向服務器上傳參數(shù),可以上傳參數(shù)的大小比GET大,且更加私密(注意是私密,不等同安全,無論哪種方法上傳參數(shù),都可以通過網(wǎng)絡抓包工具抓到)。

3.PUT方法:
用于傳輸文件,將請求報文主體中的文件保存到請求URL指定的位置,在某些情況下,如RESTfulAPI中,可以用于更新資源

4.HEAD方法:
與GET方法類似,但不返回報文主體部分,僅返回響應頭,用于確認URL的有效性及資源更新的日期時間等

5.DELETE方法:
用于刪除文件,按請求URL刪除指定的資源,是PUT的相反方法

6.OPTIONS方法:
用于查詢針對請求URL指定的資源支持的方法,返回允許的方法,如GET、POST等

這些方法中比較常用的方法是POST和GET。

HTTP的狀態(tài)碼

在這里插入圖片描述
最常見的狀態(tài)碼,比如200(OK),404(NotFound),403(Forbidden),302(Redirect,重定向),504(BadGateway)
在這里插入圖片描述
以下是僅包含重定向相關狀態(tài)碼的表格:
在這里插入圖片描述
當服務器返回 HTTP 301 狀態(tài)碼時,表示請求的資源已經(jīng)被永久移動到新的位置,在這種情況下,服務器會在響應中添加一個 Location 頭部,用于指定資源的新位置。這個 Location 頭部包含了新的 URL 地址,瀏覽器會自動重定向到該地址,例如:

HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

當服務器返回 HTTP 302 狀態(tài)碼時,表示請求的資源臨時被移動到新的位置,同樣地,服務器也會在響應中添加一個 Location 頭部來指定資源的新位置。瀏覽器會暫時使用新的 URL 進行后續(xù)的請求,但不會緩存這個重定向,例如:

HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

總之,無論是 HTTP 301 還是 HTTP 302 重定向,都需要依賴 Location 選項來指定資源的新位置。這個 Location 選項是一個標準的 HTTP 響應頭部,用于告訴瀏覽器應該將請求重定向到哪個新的 URL 地址。

HTTP的常見Header

Content-Type: 數(shù)據(jù)類型(text/html 等) ,詳細信息可以查看Content-Type
Content-Length: Body 的長度
Host: 客戶端告知服務器, 所請求的資源是在哪個主機的哪個端口上;
User-Agent: 聲明用戶的操作系統(tǒng)和瀏覽器版本信息;
referer 當前頁面是從哪個頁面跳轉過來的;
Location: 搭配 3xx 狀態(tài)碼使用, 告訴客戶端接下來要去哪里訪問; Cookie: 用于在客戶端存儲少量信息. 通常用于實現(xiàn)會話(session)的功能;
以上是HTTP常用的Header,更多的Header如下:
在這里插入圖片描述

HTTP 中的 Connection 字段是 HTTP 報文頭的一部分,它主要用于控制和管理客戶端與服務器之間的連接狀態(tài),核心作用是管理持久連接(也稱為長連接):持久連接允許客戶端和服務器在請求/響應完成后不立即關閉 TCP 連接,以便在同一個連接上發(fā)送多個請求和接收多個響應。

在 HTTP/1.1 協(xié)議中,默認使用持久連接。當客戶端和服務器都不明確指定關閉連接時,連接將保持打開狀態(tài),以便后續(xù)的請求和響應可以復用同一個連接。
在 HTTP/1.0 協(xié)議中,默認連接是非持久的。如果希望在 HTTP/1.0
上實現(xiàn)持久連接,需要在請求頭中顯式設置 Connection: keep-alive。
語法格式:

  • Connection: keep-alive:表示希望保持連接以復用 TCP 連接。
  • Connection: close:表示請求/響應完成后,應該關閉 TCP 連接

如果用戶通過瀏覽器訪問服務端,瀏覽器會自動發(fā)起以下請求獲取網(wǎng)站圖標:

GET /favicon.ico HTTP/1.1

favicon.ico 是一個網(wǎng)站圖標,通常顯示在瀏覽器的標簽頁上、地址欄旁邊或收藏夾中。這個圖標的文件名 favicon 是 “favorite icon” 的縮寫,而 .ico 是圖標的文件格式,瀏覽器在發(fā)起請求的時候,也會為了獲取圖標而專門構建 http 請求。

HTTP cookie與HTTP session

Http協(xié)議本身是無狀態(tài)和無連接的(無連接即http協(xié)議在客戶端收到服務端的應答之后就會斷開連接,后面引入了持久連接來解決該性能問題,無狀態(tài)即協(xié)議本身不緩存任何客戶端歷史上的請求信息),如果服務端想要識別用戶身份,就需要用到cookie和session技術(也合稱為會話管理技術)。

HTTP cookie

HTTP Cookie(也稱為 Web Cookie、瀏覽器 Cookie 或簡稱 Cookie)是服務器發(fā)送到用戶瀏覽器并保存在瀏覽器上的一小塊數(shù)據(jù),之后在瀏覽器向同一服務器再次發(fā)起請求時cookie被攜帶并發(fā)送到服務器上。通常,它用于告知服務端兩個請求是否來自同一瀏覽器,如用戶認證和會話管理、 跟蹤用戶行為、保持用戶的登錄狀態(tài)、記錄用戶偏好等。

當用戶第一次訪問網(wǎng)站時,服務器會在響應的 HTTP 頭中設置 Set-Cookie字段,用于發(fā)送 Cookie 到用戶的瀏覽器,瀏覽器在接收到 Cookie 后,會將其保存起來(可能是內(nèi)存級存儲,也可能是文件級存儲,通常是按照域名進行本地存儲),在之后的請求中,瀏覽器會自動在 HTTP 請求頭中攜帶 Cookie 字段,將之前保存的 Cookie 信息發(fā)送給服務器。
在這里插入圖片描述

cookie有會話 Cookie與持久 Cookie(Persistent Cookie)之分(即內(nèi)存級cookie和文件級cookie):

  • 會話 Cookie(Session Cookie):在瀏覽器關閉時失效。
  • 持久 Cookie(Persistent Cookie):帶有明確的過期日期或持續(xù)時間,可以跨多個瀏覽器會話存在,可以長時間保存。

如果 cookie 是一個持久性的 cookie,那么它其實就是瀏覽器相關的特定目錄下的一個文件。但直接查看這些文件可能會看到亂碼或無法讀取的內(nèi)容,因為 cookie 文件通常以二進制或 sqlite 格式存儲。一般在查看cookie時,直接在瀏覽器對應的選項中直接查看即可。

HTTP 存在一個報頭選項:Set-Cookie, 可以用來進行給瀏覽器設置 Cookie值。在 HTTP 響應頭中添加該header并設置好對應的值,客戶端(如瀏覽器)獲取該響應后自行設置并保存,如下設置一個cookie:

Set-Cookie: key=value; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/; domain=.example.com; secure; HttpOnly

key和value時一對鍵值對,key被用作cookie的名稱,value是cookie的值,其余的是cookie的屬性,如果需要多個key–value鍵值對,只能多次添加Set-Cookie并設置好對應屬性,而不能放在一個Set-Cookie中。cookie的屬性是可選的,每個 Cookie 屬性都以分號(;)和空格( )分隔,屬性和值之間使用等號(=)分隔,如果 Cookie 的名稱或值包含特殊字符(如空格、分號、逗號等),則需要進行 URL 編碼,關于各個屬性的解釋:

expires=<date> :設置 Cookie 的過期日期/時間,如果未指定此屬性,則 Cookie 默認為會話 Cookie,即當瀏覽器關閉時cookie就過期。時間格式必須遵守 RFC 1123 標準,具體格式樣例:

Tue, 01 Jan 2030 12:34:01 GMT/UTC

GMT(格林威治標準時間)和 UTC(協(xié)調(diào)世界時)是兩個不同的時間標準,但它們在大多數(shù)情況下非常接近。

GMT 是格林威治標準時間的縮寫,它是以英國倫敦的格林威治區(qū)為基準的世界時間標準,不受夏令時或其他因素的影響,通常用于航海、航空、科學、天文等領域,其計算方式是基于地球的自轉和公轉。

UTC 全稱為“協(xié)調(diào)世界時”,是國際電信聯(lián)盟(ITU)制定和維護的標準時間,UTC 的計算方式是基于原子鐘,而不是地球的自轉,因此它比 GMT 更準確,現(xiàn)在用的時間標準,多數(shù)全球性的網(wǎng)絡和軟件系統(tǒng)將其作為標準時間。因此我們推薦使用UTC。

path=<some_path> :定義 Cookie 的作用范圍。如果設置為根路徑/,意味著 Cookie對服務端域名下的所有路徑都可用。

domain=<domain_name> :指定哪些主機可以接受該 Cookie,默認為設置它的主機,點前綴(.)表示包括所有子域名。

secure :僅當使用 HTTPS 協(xié)議時才發(fā)送 Cookie。這有助于防止
Cookie 在不安全的 HTTP 連接中被截獲。

HttpOnly :標記 Cookie 為 HttpOnly,意味著該 Cookie 不能被
客戶端腳本(如 JavaScript)訪問。這有助于防止跨站腳本攻擊(XSS)。

HTTP session

使用cookie時,由于這些用戶私密數(shù)據(jù)在瀏覽器(用戶端)保存,非常容易被人盜取,如果寫入的是用戶的私密數(shù)據(jù),比如用戶名密碼等,就很容易被非法用戶冒充用戶登錄服務端,其次就是用戶在cookie中的私密數(shù)據(jù)可以被解析讀取,從而造成私密信息泄漏。為了解決這些問題,引入了HTTP session,一般用于用戶認證和會話管理、存儲用戶的臨時數(shù)據(jù)(如購物車內(nèi)容)、實現(xiàn)分布式系統(tǒng)的會話共享(通過將會話數(shù)據(jù)存儲在共享數(shù)據(jù)庫或緩存中)等。

HTTP Session 是服務器用來跟蹤用戶與服務器交互期間用戶狀態(tài)的機制。由于 HTTP協(xié)議是無狀態(tài)的(每個請求都是獨立的),因此服務器需要通過 Session 來記住用戶的信息。當用戶首次訪問網(wǎng)站時,服務器會為用戶創(chuàng)建一個唯一的 Session ID,并通過Cookie 將其發(fā)送到客戶端??蛻舳嗽谥蟮恼埱笾袝y帶這個 Session ID,服務器通過 Session ID 來識別用戶,從而獲取用戶的會話信息。服務器通常會將 Session 信息存儲在內(nèi)存、數(shù)據(jù)庫或緩存中。

總的來說就是服務端用一條記錄記錄了用戶信息,并用一個唯一值與該記錄相關聯(lián),接著服務端將該唯一值通過cookie保存在用戶瀏覽器端,當用戶請求服務時客戶端將該唯一值發(fā)給服務端,服務端就可以利用該唯一值在自己的數(shù)據(jù)庫中尋找用戶的記錄,最后做對應的處理。

由于用戶私密信息的記錄是保存在服務端,而服務端一般都有不錯的安全防護,因此這樣基本可以解決用戶私密信息泄漏問題。服務端還可以通過檢測session是否異常,以決定是否讓session失效,例如檢測到訪問用戶的地址發(fā)生改變就判定session異常,這樣就算瀏覽器的cooki被盜取了,服務端讓該session失效非法用戶就無法冒充用戶了。

因此Cookie 是存儲在客戶端的,而 Session 是存儲在服務器端的。它們各有優(yōu)缺點,通常在實際應用中會結合使用,以達到最佳的用戶體驗和安全性。

HTTPS協(xié)議原理

HTTP 協(xié)議內(nèi)容都是按照文本的方式明文傳輸?shù)?#xff0c;明文數(shù)據(jù)會經(jīng)過路由器、wifi 熱點、通信服務運營商、代理服務器等多個物理節(jié)點,如果信息在傳輸過程中被劫持,傳輸?shù)膬?nèi)容就完全暴露了。劫持者還可以篡改傳輸?shù)男畔⑶也槐浑p方察覺,這就是中間人攻擊,所以我們有對信息進行加密的需求,由此產(chǎn)生了HTTPS協(xié)議。HTTPS 也是一個應用層協(xié)議,是在 HTTP 協(xié)議的基礎上引入了一個加密層。在這里插入圖片描述
加密就是把明文(要傳輸?shù)男畔?#xff09;進行一系列變換生成密文,解密就是把密文再進行一系列變換還原成明文,在這個加密和解密的過程中,往往需要一個或者多個中間的數(shù)據(jù),輔助進行這個過程,這樣的數(shù)據(jù)稱為密鑰。
常見的加密方式有2種:對稱加密和非對稱加密

  1. 對稱加密

對稱加密就是采用單鑰密碼系統(tǒng)的加密方法,只要同一個密鑰就可以同時用作信息的加密和解密,這種加密方法也稱為單密鑰加密,其特征就是加密和解密所用的密鑰是相同的。常見對稱加密算法有:DES、3DES、AES、TDEA、Blowfish、RC2 等,采用這種加密方式有許多優(yōu)點:計算量?、加密速度快、加密效率高。

對稱加密其實就是通過同一個 “密鑰” , 把明文加密成密文, 并且也能把密文解密成明文,例如以下一個簡單的對稱加密:按位異或
假設明文 a = 1234,密鑰 key = 8888
則加密 a ^ key 得到的密文 b 為 9834. 然后針對密文 9834 再次進行運算 b ^ key,得到的就是原來的明文 1234. (對于字符串的對稱加密也是同理, 每一個字符都可以表?成一個數(shù)字),按位異或只是最簡單的對稱加密,HTTPS 中并不是使用按位異或。

  1. 非對稱加密

非對稱加密就是需要兩個密鑰來進行加密和解密的加密方法,這兩個密鑰分別是是公開密鑰(public key,簡稱公鑰,允許被別人獲取)和私有密鑰(private key,簡稱私鑰,絕對不能泄漏),通常是一個用于加密,另一個用于解密(例如通過公鑰對明文加密變成密文,再通過私鑰對密文解密,變成明文,這種用法下只有私鑰可以對公鑰加密出來的密文進行正確解密。也可以反著用,通過私鑰對明文加密變成密文,通過公鑰對密文解密,變成明文,這種用法下只有私鑰加密出來的密文才能被公鑰正確解密)。常見非對稱加密算法有:RSA,DSA,ECDSA等,其數(shù)學原理比較復雜,涉及到一些數(shù)論相關的知識。這種加密方法算法強度復雜,安全性依賴于算法與密鑰,加密解密速度沒有對稱加密解密的速度快(通常是慢很多)。

下面我們通過對比幾種解決方案,以便得到最佳解決方案:

  1. 方案1:只采用對稱加密

在這里插入圖片描述
雙方想進行對稱加密通信,當客戶端生成對稱密鑰C后,由于服務端不知道該密鑰,因此客戶端就必須要先將該密鑰C發(fā)送給服務端,此時中間人就可以直接竊取到該密鑰C,所以后續(xù)雙方利用密鑰C進行通信時,中間人都可以利用竊取到的密鑰C將密文解密成明文。因此這種方案不可行。

  1. 方案2:只采用非對稱加密

在這里插入圖片描述
客戶端生成公鑰C和私鑰C’后,想用C進行加密,C’進行解密,于是客戶端將公鑰C發(fā)送給服務端,服務端和中間人都得到了公鑰C,當服務端用得到的C對應答加密返回給客戶端時,盡管中間人有公鑰C,但其沒有私鑰C’,就無法對該密文進行解密,因此目前來看服務端到客戶端的通信是安全的(后面會說明其實這也并不安全),但客戶端到服務端的通信依舊可以被中間人竊取解讀,而且通信采用非對稱加密,通信速度慢,因此這種方案不可行。

  1. 方案3:雙方都采用非對稱加密

基于方案2,我們可以讓客戶端生成公鑰C和私鑰C’,服務端也生成自己的公鑰S和私鑰S’,雙方在通信前先進行公鑰交換,中間人進行得到了公鑰C和S,但由于沒有對應的私鑰自然就無法解讀后續(xù)加密的密文,因此目前來看服務端到客戶端的通信是安全的(同方案2一樣,后面會說明其實這也并不安全),而且通信雙方都采用非對稱加密,通信速度十分緩慢,因此這種方案不可行。

  1. 方案4:采用非對稱加密和對稱加密

服務端生成自己的公鑰S和私鑰S’,客戶端先獲取服務端的公鑰S,接著生成對稱密鑰X,通過S將對稱密鑰X加密發(fā)送給服務端,服務端就可以利用S’對密文解密得到對稱密鑰X,這樣就只有客戶端和服務端知道對稱密鑰X,此后雙方就可以利用對稱密鑰X進行通信了,這種加密方式目前看來是安全的,而且通信時采用對稱加密,通信速度快,接下來說明為什么這種加密方式不安全:

在這里插入圖片描述
這也說明了為什么方案2和方案3的通信是不安全的,因為中間人完全可以冒充客戶端和服務器向另一方發(fā)送自己的公鑰。

  1. 方案5:采用非對稱加密+對稱加密+證書認證

現(xiàn)在我們清楚方案4的致命缺陷在于客戶端不清楚一開始接收到的公鑰是服務端的還是中間人的,如果客戶端可以甄別出接收到的公鑰是服務端的再將對稱密鑰X利用得到的公鑰加密發(fā)送給服務端,那么中間人就無法解密出密鑰X,此后的客戶端和服務端就可以安全的通信了。

在了解方案5之前我們需要知道什么是數(shù)字指紋、數(shù)據(jù)簽名和證書。

數(shù)據(jù)指紋,或者說數(shù)據(jù)摘要,其基本原理是利用單向散列函數(shù)(Hash 函數(shù))對信息進行運算,生成一串固定長度的數(shù)字摘要(兩個不同的信息,有可能算出相同的摘要,但是概率非常低),數(shù)字指紋并不是一種加密機制(因為沒有解密),從摘要很難反推原信息,而且源字符串只要改變一點點,最終得到的數(shù)據(jù)摘要值都會差別很大,通常用來進行數(shù)據(jù)對比,以判斷數(shù)據(jù)有沒有被篡改,摘要常見算法有:MD5、SHA1、SHA256、SHA512 等。

數(shù)據(jù)簽名就是簽名者(只有CA機構有資格成為簽名者)利用自己的私鑰對一份數(shù)據(jù)(一般是數(shù)據(jù)摘要,這樣可以縮小簽名密文的長度,加快數(shù)字簽名的驗證簽名的運算速度)進行加密形成的一份密文。

證書就是一份明文數(shù)據(jù)拼接上該數(shù)據(jù)對應的簽名。

服務端在使用 HTTPS 前,先生成公鑰和私鑰,私有服務端持有,然后服務端負責人向 CA 機構申領一份數(shù)字證書(申請證書的時候,需要在特定平臺生成,例如CSR,會同時生成一對密鑰對,即公鑰和私鑰。這對密鑰對就是用來在網(wǎng)絡通信中進行明文加密以及數(shù)字簽名的。其中公鑰會隨著 CSR 文件,一起發(fā)給 CA 進行權威認證,私鑰服務端自己保留,用來后續(xù)進行通信),CA機構會根據(jù)申請者提交的域名、公鑰等信息根據(jù)自己的私鑰生成一份簽名,再將簽名和提交的明文信息拼接就形成了一份CA證書。
在這里插入圖片描述

當客戶端首次向服務端發(fā)起請求時,服務端直接給客戶端返回一份CA證書,客戶端(可以認為是瀏覽器)一般內(nèi)置了CA機構對應的公鑰,其會用該公鑰對簽名解密得到一份數(shù)據(jù)摘要A,然后使用同樣的哈希算法對明文數(shù)據(jù)進行運算得到一份數(shù)據(jù)摘要B,客戶端只需要比較A和B是否相等就可以知道該證書是否被修改,未修改就可以根據(jù)證書得到服務端的公鑰,然后采用方案4的思路進行通信就可以了。

由于客戶端使用的是CA機構的公鑰解密,因此中間人對證書的任何修改客戶端都可以知道,例如中間人向篡改明文數(shù)據(jù)中的公鑰,由于中間人沒有CA機構的私鑰自然就無法將篡改后的明文數(shù)據(jù)形成對應的簽名。盡管中間人也可以向CA機構申請一份證書,然后利用該申請的證書冒充服務端發(fā)給客戶端,但客戶端只需要再檢查對應的域名是否正確就可以識別出是否是服務端返回的證書。

因此方案5可以保證通信安全,且通信速度也較快。

在這里插入圖片描述
HTTPS 工作過程中涉及到的密鑰有三組:

第一組(非對稱加密):用于校驗證書是否被篡改,CA機構持有私鑰,客戶端持有公鑰(操作系統(tǒng)包含了可信任的 CA 認證機構有哪些,同時持有對應的公鑰),服務器在客戶端請求時,返回攜帶簽名的證書,客戶端通過這個公鑰進行證書驗證,保證證書的合法性,進一步保證證書中攜帶的服務端公鑰權威性。

第?組(非對稱加密):用于協(xié)商生成對稱加密的密鑰,客戶端用收到的 CA 證書中的公鑰(是可被信任的)給隨機生成的對稱加密的密鑰加密,傳輸給服務器,服務器通過私鑰解密獲取到對稱加密密鑰。

第三組(對稱加密):客戶端和服務器后續(xù)傳輸?shù)臄?shù)據(jù)都通過這個對稱密鑰加密解密。

傳輸層

UDP

UDP協(xié)議端的格式如下:
在這里插入圖片描述
源/目的端口號:表示數(shù)據(jù)是從哪個進程來,到哪個進程去

16位UDP長度:表示整個數(shù)據(jù)報(UDP 首部+UDP 數(shù)據(jù))的長度,以字節(jié)為單位,其至少為8字節(jié),否則該UDP報文被視為無效,可以知道整個UDP報文的最大長度是64K,如果用戶想要發(fā)送的數(shù)據(jù)超過了64K,需要自己手動實現(xiàn)對報文進行分片,然后手動實現(xiàn)拼裝接收到的報文。

16位UDP檢驗和:用于檢驗UDP報文是否出錯,如果校驗和出錯,就會直接丟棄。其實就是發(fā)送方先按照一定的規(guī)則對這個UDP報文進行計算得到一個檢驗和,接收方也按照相同的規(guī)則對接收到的報文進行計算,如果如果計算出來的檢驗和與UDP數(shù)據(jù)報上攜帶的檢驗和不一致,則認為UDP數(shù)據(jù)包傳輸出錯。

UDP的特點如下:

  1. 無連接:知道目的的 IP 和端口號就直接進行傳輸,不需要建立連接
  2. 不可靠:沒有確認機制,沒有重傳機制,如果因為網(wǎng)絡故障該段無法發(fā)到對方, UDP 協(xié)議層也不會給應用層返回任何錯誤信息
  3. 面向數(shù)據(jù)報:不能夠靈活的控制讀寫數(shù)據(jù)的次數(shù)和數(shù)量,應用層交給 UDP 多長的報文, UDP 原樣發(fā)送,既不會拆分,也不會合并,例如:用 UDP傳輸100 個字節(jié)的數(shù)據(jù),如果發(fā)送端調(diào)用一次 sendto,發(fā)送100個字節(jié),那么接收端也必須調(diào)用對應的一次 recvfrom,接收100個字節(jié),而不能循環(huán)調(diào)用 10 次 recvfrom,每次接收 10 個字節(jié)。

需要注意的是UDP 沒有真正意義上的發(fā)送緩沖區(qū),調(diào)用 sendto 會直接交給內(nèi)核,由內(nèi)核將數(shù)據(jù)傳給網(wǎng)絡層協(xié)議進行后續(xù)的傳輸動作,但UDP 具有接收緩沖區(qū),但是這個接收緩沖區(qū)不能保證收到的 UDP 報的順序和
發(fā)送 UDP 報的順序一致,如果緩沖區(qū)滿了,再到達的 UDP 數(shù)據(jù)就會被丟棄。

基于UDP的應用層協(xié)議有:

  • NFS:網(wǎng)絡文件系統(tǒng)
  • TFTP:簡單文件傳輸協(xié)議
  • DHCP:動態(tài)主機配置協(xié)議
  • BOOTP:啟動協(xié)議(用于無盤設備啟動)
  • DNS:域名解析協(xié)議

TCP

tcp一個文件描述符對用2個緩沖區(qū),支持全雙工通信,udp不是全雙工,但可以在應用層添加特性來保證全雙工。

TCP協(xié)議端的格式

TCP協(xié)議端的格式如下:
在這里插入圖片描述

源/目的端口號:表示數(shù)據(jù)是從哪個進程來,到哪個進程去

4位首部長度:表示該 TCP 頭部有多少個32位bit(有多少個4字節(jié)),所以TCP 頭部最大長度是15 * 4 = 60字節(jié)

6位標志位

  • URG:Urgent Pointer,緊急指針是否有效
  • ACK:Acknowledgment,確認序號是否有效
  • PSH:Push,提示接收端應用程序立刻從 TCP 緩沖區(qū)把數(shù)據(jù)讀走
  • RST:Reset,對方要求重新建立連接,我們把攜帶 RST 標識的報文稱為復位報文段
  • SYN:Synchronize,請求建立連接; 我們把攜帶 SYN 標識的稱為同步報文段
  • FIN:Finish,通知對方本端要關閉了,我們稱攜帶 FIN 標識的報文為結束報文段

這些標志位是為了區(qū)分報文類型,例如有的報文是為了建立連接,有的是斷開連接,有的則是正常通信等,以便對方?jīng)Q定接下來的動作

16位校驗和:發(fā)送端填充, CRC 校驗. 接收端校驗不通過, 則認為數(shù)據(jù)有問題. 此處的檢驗和不光包含 TCP 首部, 也包含 TCP 數(shù)據(jù)部分,如果檢驗出錯則丟棄當前報文,在ACK應答中要求重新發(fā)送該報文

16位緊急指針:緊急指針字段的值為緊急數(shù)據(jù)最后一個字節(jié)相對于TCP報文段首部的序列號的偏移量。通過將該偏移量與序列號相加,可以計算出緊急數(shù)據(jù)最后一個字節(jié)的序號,從而定位緊急數(shù)據(jù)在報文段中的位置,由于緊急數(shù)據(jù)的處理可能會打斷正常的TCP流量控制機制,并且TCP協(xié)議本身并沒有為緊急數(shù)據(jù)提供額外的保護(如重傳機制),因此在實際應用中需要謹慎使用

40字節(jié)頭部選項:為TCP連接提供了額外的功能和靈活性,使得TCP能夠適應不同的網(wǎng)絡環(huán)境和應用需求,從而提高網(wǎng)絡傳輸?shù)男屎涂煽啃?#xff0c;最大為40字節(jié)

其余字段后面作解釋。

TCP的報文格式之所以比UDP的復雜很多,就是因為TCP要保證可靠性傳輸,其引入了各種機制來保證tcp的可靠性。

確認應答機制

現(xiàn)在主機A向主機B發(fā)送了一個tcp報文段,那么主機A怎么確定主機B是否收到了報文呢?最樸素的做法就是主機B在接收到報文后向主機A發(fā)送一個應答報文,告訴主機A自己已經(jīng)收到報文了,如果主機A收到了主機B的應答,主機A就可以保證主機B一定收到了報文。tcp的應答報文就是將報頭的ACK標志位置1,因此因此應答報文也稱為ACK報文。

現(xiàn)在又來了一個新問題:如果tcp接連發(fā)送了2個報文,由于報文在網(wǎng)絡中可能會選擇不同的路徑,從而造成這2個報文到達主機B的順序不一定與主機A的發(fā)送順序一致,那主機B如何保證接收報文的順序呢?這就需要用到tcp報頭的32位序號和32位確認序號。

tcp層有兩個緩沖區(qū),一個發(fā)送緩沖區(qū),一個接收緩沖區(qū)(其實send,recv等調(diào)用就是拷貝函數(shù),將數(shù)據(jù)拷貝到發(fā)送緩沖區(qū)或從接收緩沖區(qū)拷貝數(shù)據(jù)出去),要發(fā)送的數(shù)據(jù)會提前放到發(fā)送緩沖區(qū)中,我們把該緩沖區(qū)想象成一個大數(shù)組,tcp會為每一個數(shù)據(jù)進行編號,這就是序列號。
在這里插入圖片描述

主機A在發(fā)送數(shù)據(jù)時會在報頭的32位序號中填充該TCP報文段中第一個字節(jié)的序號,主機B在收到報文后按照序號的大小排序就可以確定接收到的數(shù)據(jù)報的順序,同時主機B會在應答報文的32位確認序號中填充下一個期望接收的字節(jié)的序號,即告訴主機A下次該從哪里發(fā)。

在這里插入圖片描述

通過確認應答機制,主機B就可以告訴主機A自己已經(jīng)收到了那些數(shù)據(jù),下一次主機A應該從哪里開始發(fā)。

超時重傳機制

主機A通過接收主機B的應答來確認B已經(jīng)接收到的報文,如果主機A沒有接收到B的應答呢?tcp的解決辦法是如果超過一定的時間主機A沒有接收到主機B的應答,就對沒有應答的報文進行重發(fā),那么該如何確定多長時間算超時呢?

應對策略是找到一個最小的時間,保證確認應答一定能在這個時間內(nèi)返回,但是這個時間的長短隨著網(wǎng)絡環(huán)境的不同是有差異的,如果超時時間設的太長,會影響整體的重傳效率,如果超時時間設的太短,有可能會頻繁發(fā)送重復的包。TCP 為了保證無論在任何環(huán)境下都能比較高性能的通信,選擇動態(tài)計算這個超時時間, Linux中(BSD Unix 和 Windows 也是如此),超時以 500ms 為一個單位進行控制,每次判定超時重發(fā)的超時時間都是 500ms 的整數(shù)倍,例如如果重發(fā)一次之后仍然得不到應答,等待 2*500ms 后再進行重傳,如果仍然得不到應答就等待 4*500ms 進行重傳,依次類推,以指數(shù)方式遞增,當累計到一定的重傳次數(shù), TCP 認為網(wǎng)絡或者對端主機出現(xiàn)異常,強制關閉連接。

現(xiàn)在又來了一個問題,主機A沒有收到應答,不一定是主機A發(fā)送的數(shù)據(jù)段丟失了,有可能是主機B接收到了A的報文,但應答丟失了,也有可能是應答正阻塞在某個路由中導致超時,無論哪種情況,主機A都會選擇重發(fā)導致主機B收到多份重復的報文,主機B該如何對報文去重呢?

解決辦法很簡單,根據(jù)接收到的報文的32位序號就可以做到去重的功能了。所有接收到的數(shù)據(jù)段都會先放在tcp層的接收緩沖區(qū)中,如果數(shù)據(jù)段還在接收端的TCP緩沖區(qū)中,但此時又收到了一個具有相同序列號的數(shù)據(jù)段,TCP接收端會檢查新收到的數(shù)據(jù)段的序列號。如果序列號與緩沖區(qū)中某個已接收但尚未被上層應用讀取的數(shù)據(jù)段序列號相同,則認為該數(shù)據(jù)段是重復的,此時直接丟棄新來的數(shù)據(jù)段就可以了;如果某個數(shù)據(jù)段已經(jīng)被上層讀走了,由于TCP的32位序列號是一個連續(xù)的、遞增的序列,用于標識每個數(shù)據(jù)段的順序,即使某個數(shù)據(jù)段已經(jīng)被上層應用讀取,TCP接收端仍然會跟蹤序列號的狀態(tài),如果數(shù)據(jù)段的序列號已經(jīng)超過了它最近一次發(fā)送的ACK中的序列號,tcp就認為該數(shù)據(jù)段是重復的,直接丟棄這個數(shù)據(jù)段就可以了。

連接管理機制

我們知道tcp是面向連接的,那么tcp是如何保證連接已建立好了的呢?正常情況下,tcp要經(jīng)歷3次握手建立連接,4次揮手斷開連接。

建立連接

tcp三次握手的流程為:第一,客戶端先向服務端發(fā)送一個攜帶SYN標志位的報文,以告訴服務端自己有建立連接的請求,第二,服務端接收到客戶端請求建立連接的報文后,發(fā)送一個攜帶ACK和SYN標志位的報文,以對客戶端發(fā)來的報文做應答,同時表示自己也希望和客戶端建立連接,第三,客戶端收到服務端的報文后,發(fā)送一個ACK報文以對服務端發(fā)來的報文做應答。
在這里插入圖片描述

如果一開始報文①就丟失了導致服務端沒有收到,服務端自然就不會應答,客戶端超時收不到應答就會重新發(fā)送報文①,如果第一次握手一直不成功,達到最大重傳次數(shù)客戶端就會放棄連接。

如果報文②丟失了,客戶端收不到應答就會重新發(fā)送報文①,如果客戶端在達到最大重傳次數(shù)后仍未收到服務端的報文②,客戶端會斷開TCP連接。對于服務端來說,服務端等不到客戶端的應答③,服務端就會重新發(fā)送報文②,如果服務端在達到最大重傳次數(shù)后仍未收到客戶端的ACK報文③,服務端會釋放之前為這次連接分配的資源。

如果報文③丟失了,服務端等不到客戶端的應答,服務端就會重新發(fā)送報文②,這樣客戶端就會重發(fā)報文③,如果服務端在達到最大重傳次數(shù)后仍未收到客戶端的ACK報文③,它會釋放之前為這次連接分配的資源,并關閉連接。此時還存在一種情況,由于此時客戶端認為連接已經(jīng)建立好了,其可能會直接向服務端發(fā)送數(shù)據(jù),在這種服務端在沒有收到ACK報文就接收到客戶端的數(shù)據(jù)的情況下,服務端會給客戶端發(fā)送一個攜帶RST標志位的報文,通知客戶端重新建立連接,當客戶端收到RST報文時,它會意識到連接已經(jīng)被強制關閉,并且通常會釋放與該連接相關的所有資源,如果客戶端需要繼續(xù)與服務端通信,它將需要重新進行TCP的三次握手來建立一個新的連接。

如果三次握手順利,客戶端和服務端都有一次報文的收發(fā),這樣雙方都可以確定自己發(fā)送的報文對方可以收到,對方發(fā)送的報文自己也可以收到,從而確認信道是健康的,同時確保了雙方的TCP都是愿意通信的。需要注意的是三次握手不是保證100%建立連接,而是經(jīng)歷過三次握手之后,雙方都認為連接建立好了。而且第一次握手和第二次握手的報文不允許攜帶數(shù)據(jù),允許第三次握手的報文攜帶數(shù)據(jù),這樣做一方面是第一次握手和第二次握手雙方都不認為連接已經(jīng)建立,另一方面是為了阻止惡意攻擊:客戶端可能會直接向服務端發(fā)送大量攜帶垃圾數(shù)據(jù)的SYN報文,而服務端只能被動的接收這些垃圾數(shù)據(jù),從而使服務端遭受攻擊。

至于我們?yōu)槭裁催x擇三次握手而不選擇一次或兩次握手,是因為這種方式雙方不能保證或者只有一方可以保證信道健康,而不選擇四次或者更多次握手單純就是因為沒有必要。

tcp在進行連接的過程中,服務端和客戶端都存在相應的狀態(tài)的變化:

  1. 服務端的狀態(tài)變化:
  • [CLOSED -> LISTEN] 服務器端調(diào)用 listen 后進入 LISTEN 狀態(tài), 等待客戶端連接
  • [LISTEN -> SYN_RCVD] 一旦監(jiān)聽到連接請求(同步報文段),就將該連接放入內(nèi)核等待隊列中, 并向客戶端發(fā)送 SYN +ACK確認報文
  • [SYN_RCVD -> ESTABLISHED] 服務端一旦收到客戶端的確認報文,就進入ESTABLISHED 狀態(tài),可以進行讀寫數(shù)據(jù)了
  1. 客戶端的狀態(tài)變化:
  • [CLOSED -> SYN_SENT] 客戶端調(diào)用 connect,發(fā)送同步報文段
  • [SYN_SENT -> ESTABLISHED] connect 調(diào)用成功,即客戶端接收到服務端的確認報文,則進入 ESTABLISHED 狀態(tài),可以開始讀寫數(shù)據(jù)

在這里插入圖片描述

斷開連接

TCP四次揮手的過程為:第一,客戶端先向服務端發(fā)送一個攜帶FIN標志位的報文,以告訴服務端自己現(xiàn)在希望斷開連接;第二,服務端收到客戶端請求斷開連接的報文后,向客戶端發(fā)送一個ACK應答報文,表示自己已收到客戶端斷開連接的請求;第三,服務端也向客戶端發(fā)送一個攜帶FIN標志位的報文,表示自己要和客戶端斷開連接;第四,客戶端收到服務端請求斷開連接的報文后發(fā)送一個ACK報文,表示自己已收到服務端斷開連接的請求。

需要注意的是,發(fā)送FIN報文只是表示自己在斷開連接之前不再向對方發(fā)送數(shù)據(jù)了,但這期間依然可以正常接收數(shù)據(jù)。
在這里插入圖片描述
如果報文①丟失了導致服務端沒有收到客戶端希望斷開連接的請求,服務端自然就不會應答,客戶端超時收不到應答就會重新發(fā)送報文①,如果重傳次數(shù)達到了最大值,但仍然沒有收到ACK應答報文,那么客戶端可能會認為連接已經(jīng)不可恢復,于是會關閉連接。

如果報文②丟失了,客戶端觸發(fā)重傳機制,重發(fā)報文①,如果重傳次數(shù)達到了最大值,但仍然沒有收到ACK應答報文,那么客戶端可能會認為連接已經(jīng)不可恢復,于是會關閉連接。

如果報文③丟失了,對服務端來說,它遲遲收不到客戶端的應答,則觸發(fā)服務端的超時重傳,于是服務端重發(fā)報文③,如果在重傳多次后,服務端仍然無法收到客戶端的ACK報文,服務端會斷開連接。對客戶端來說,客戶端會等待一段時間,默認通常是60秒,如果仍未收到服務端的報文③,客戶端會自動關閉連接。

如果報文④丟失了,對服務端來說,它遲遲收不到客戶端的應答,則觸發(fā)服務端的超時重傳,于是服務端重發(fā)報文③,如果在重傳多次后,服務端仍然無法收到客戶端的ACK報文,服務端會斷開連接。對客戶端來說,客戶端在發(fā)送ACK報文后,它不會立即關閉連接,而是通常會進入TIME-WAIT狀態(tài),等待足夠的時間(通常是2倍的MSL,即Maximum Segment Lifetime,最大報文段生存時間)以確保服務端能夠收到ACK報文,客戶端如果又收到了服務端的FIN報文,此時它會認為自己發(fā)送的ACK報文④丟失了,于是忽略當前接收到的報文,并重新發(fā)送一次ACK報文④以應答之前的FIN報文③,然后再等待上一段時間,當客戶端在TIME-WAIT狀態(tài)等待足夠的時間后,就會正常關閉連接。

如果四次揮手順利,雙方都可以確認對方已經(jīng)同意斷開連接,那為什么不選擇一次、兩次或者三次揮手呢?
在這里插入圖片描述
對于一次揮手,萬一報文①丟失了,服務端無法知道客戶端已經(jīng)斷開連接,其仍會向客戶端發(fā)送數(shù)據(jù),而客戶端已經(jīng)斷開連接不會對這些報文作應答,服務端就會不斷重傳直至到達最大重傳次數(shù),這明顯會造成網(wǎng)路資源的浪費,而且萬一服務端還有一些重要數(shù)據(jù)沒有發(fā)送給客戶端,可鞥會對雙方以后的運行造成影響。

對于二次揮手,由于ACK應答并不是代表服務端也要斷開連接,僅僅只是服務端告訴客戶端自己已經(jīng)收到了客戶端斷開連接的請求,因此服務端依然可能向客戶端發(fā)送數(shù)據(jù)。

對于三次揮手,盡管服務端也向客戶端發(fā)送了FIN報文,告訴客戶端自己不在向它發(fā)送數(shù)據(jù)了,同時自己也希望斷開連接,但服務端無法確認客戶端是否真正接收到了自己的FIN報文。這可能導致雙方對連接狀態(tài)的理解不一致。如果該FIN報文丟失了,客戶端也不知道服務端已經(jīng)斷開連接了,它依舊覺得服務端也許還有數(shù)據(jù)會發(fā)送給自己,于是就會白白等待上一段時間才會知道服務端斷開連接了然后自己也斷開連接,而且如果客戶端收到了服務端的FIN報文卻不做應答,這不符合TCP協(xié)議的規(guī)定??傊?#xff0c;三次揮手不符合TCP協(xié)議的全雙工通信特性和確保雙方都能有序、安全地關閉連接的需求。

最后還有一個問題,那為什么不把第二次揮手和地三次揮手合并呢?因為服務端要及時回復第二次揮手的ACK報文,但其可能還有一些數(shù)據(jù)沒有處理完,服務端需要等數(shù)據(jù)處理完并發(fā)送給客戶端之后才能斷開連接,因此第二次揮手和地三次揮手一般不能合并(從這里也可以看出,三次握手的本質(zhì)是四次握手,只不過第二次和第三次握手合并了,這其實就是捎帶應答,即ACK應答與響應數(shù)據(jù)一同攜帶發(fā)送到發(fā)送端,從而減少發(fā)送次數(shù))。

tcp在斷開連接的過程中,服務端和客戶端都存在相應的狀態(tài)的變化:

  1. 服務端狀態(tài)轉化:
  • [ESTABLISHED -> CLOSE_WAIT] 當客戶端主動關閉連接(調(diào)用 close),服務器會收到結束報文段, 服務器返回確認報文段并進入CLOSE_WAIT狀態(tài)
  • [CLOSE_WAIT -> LAST_ACK] 進入 CLOSE_WAIT后說明服務器準備關閉連接(需要處理完之前的數(shù)據(jù)),當服務器真正調(diào)用 close 關閉連接時,會向客戶端發(fā)送FIN報文,此時服務器進入 LAST_ACK 狀態(tài),等待最后一個 ACK 到來(這個 ACK 是客戶端確認收到了FIN報文)
  • [LAST_ACK -> CLOSED] 服務器收到了客戶端對 FIN 的 ACK應答,徹底關閉連接
  1. 客戶端狀態(tài)變化:
  • [ESTABLISHED -> FIN_WAIT_1] 客戶端主動調(diào)用 close 時,向服務器發(fā)送結束報文段, 同時進入 FIN_WAIT_1狀態(tài)
  • [FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務器對結束報文段的確認,進入 FIN_WAIT_2狀態(tài),開始等待服務器的結束報文段
  • [FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務器發(fā)來的結束報文段,進入TIME_WAIT狀態(tài),并發(fā)出ACK應答報文
  • [TIME_WAIT -> CLOSED] 客戶端要等待一個 2MSL(Max Segment Life,報文最大生存時間)的時間,才會進入 CLOSED 狀態(tài)

在這里插入圖片描述
關于 TIME_WAIT:
TCP 協(xié)議規(guī)定,主動關閉連接的一方要處于 TIME_ WAIT 狀態(tài),等待兩個MSL(maximum segment lifetime)的時間后才能回到 CLOSED 狀態(tài),如果是服務端終止了,那么服務端就是是主動關閉連接的一方,在
TIME_WAIT 期間其不能再次監(jiān)聽同樣的 server 端口,這也是為什么服務端要設置地址復用的原因。MSL 在 RFC1122 中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實現(xiàn)不同,在 Centos7 上默認配置的值是 60s,可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值。

為什么是 TIME_WAIT 的時間是 2MSL呢?
MSL 是 TCP 報文的最大生存時間,如果 TIME_WAIT 持續(xù)存在 2MSL 的話,就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經(jīng)消失,否則服務器立刻重啟,可能會收到來自上一個進程的遲到的數(shù)據(jù),但是這種數(shù)據(jù)很可能是錯誤的,同時也是在理論上保證最后一個報文可靠到達(假設最后一個 ACK 丟失,那么服務器會再重發(fā)一個 FIN. 這時雖然客戶端的進程不在了,但是 TCP 連接還在,仍然可以重發(fā) ACK應答)。

關于 CLOSE_WAIT:
對于服務器上出現(xiàn)大量的 CLOSE_WAIT 狀態(tài),原因就是服務器沒有正確的關閉socket,導致四次揮手沒有正確完成,這是一個 BUG,只需要加上對應的 close 即可解決問題。

滑動窗口

在確認應答機制下,對每一個發(fā)送的數(shù)據(jù)段,都要給一個 ACK 確認應答,收到 ACK 后再發(fā)送下一個數(shù)據(jù)段,這樣做明顯導致數(shù)據(jù)傳輸效率低,性能較差,尤其是數(shù)據(jù)往返的時間較長的時候。于是我們想到,既然這樣一發(fā)一收的方式性能較低,那么我們可以一次發(fā)送多條數(shù)據(jù),就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了),而接收方則對接收到的數(shù)據(jù)排序后放入其接收緩沖區(qū)中。
在這里插入圖片描述
滑動窗口是TCP層發(fā)送緩沖區(qū)的一部分,窗口大小表示無需等待確認應答而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值,只有收到對方的ACK后,滑動窗口才會根據(jù)確認應答序號做相應的向右滑動
在這里插入圖片描述
滑動窗口在內(nèi)核中其實就是用兩個指針來進行維護:

  • start_win:指向窗口左側,等于收到的確認應答的32位確認序號,需要注意,如果對方回復了某個確認序號,那么就表示確認序號之前的數(shù)據(jù)對方都已經(jīng)收到。

  • end_win:等于start_win加上窗口大小(該值大小為應答中的16窗口大小字段,是對方基于自己接收緩沖區(qū)還可以接收數(shù)據(jù)的能力填充的)

因此滑動窗口左側的數(shù)據(jù)就表示發(fā)送方已經(jīng)確定對方已經(jīng)接收到的數(shù)據(jù),在滑動窗口中的數(shù)據(jù)就表示可以發(fā)送或已經(jīng)發(fā)送但還未確認對方已經(jīng)收到的數(shù)據(jù),滑動窗口右側的數(shù)據(jù)表示待發(fā)送的數(shù)據(jù)。

如果發(fā)送的數(shù)據(jù)包丟失了,該包的32位序號是1001
在這里插入圖片描述

那么接收方接下來的所有ACK應答的確認序號都是1001,表示1001之前的數(shù)據(jù)我都已經(jīng)收到了,接下來請從1001開始發(fā),當發(fā)送方連續(xù)三次收到了同樣一個 “1001” 這樣的應答,就會將序列號為1001的數(shù)據(jù)包重新發(fā)送,這個時候接收端收到了 1001 之后,再次返回的 ACK 就是 7001 了,因為 2001 - 7000的數(shù)據(jù)接收端其實之前就已經(jīng)收到了,被放到了接收端操作系統(tǒng)內(nèi)核的接收緩沖區(qū)中。這種機制被稱為“高速重發(fā)控制”,也叫 “快重傳”,當快重傳之后依舊收到和之前相同的確認應答序號時,發(fā)送方會意識到可能存在問題,并采取相應的措施來確保數(shù)據(jù)的可靠傳輸和網(wǎng)絡的穩(wěn)定,包括繼續(xù)重傳丟失的報文段、關注接收方的窗口大小信息、采用擁塞控制機制以及根據(jù)網(wǎng)絡狀況調(diào)整發(fā)送策略等,如果問題無法解決,并且網(wǎng)絡狀況持續(xù)惡化,那么TCP最終可能會決定關閉連接。

快重傳速度比超時重傳快(因為其不需要等到超時沒有收到ACK應答時才意識到數(shù)據(jù)丟了),需要注意的是,接收方只有收到至少3次相同的確認序號時才會觸發(fā)快重傳,由超時重傳進行兜底。

如果部分ACK應答丟失了,其通常是不要緊的,因為可以通過后續(xù)的ACK應答知道接收方已經(jīng)接收了那些數(shù)據(jù),從而決定是否重發(fā):如果接收方成功接收了某個數(shù)據(jù)報文段,并且后續(xù)還有數(shù)據(jù)報文段被成功接收,那么接收方會發(fā)送包含更高確認序號的ACK應答,這個ACK應答可以間接地告訴發(fā)送方,之前丟失ACK的那個數(shù)據(jù)報文段已經(jīng)被成功接收了;如果發(fā)送方在一段時間內(nèi)沒有收到某個數(shù)據(jù)報文段的ACK應答,并且后續(xù)也沒有收到包含更高確認序號的ACK應答,那么發(fā)送方會認為這個數(shù)據(jù)報文段可能丟失了,并觸發(fā)超時重傳或快重傳機制來重新發(fā)送這個數(shù)據(jù)報文段。

到這里,我們也許還存在一些疑惑,即既然發(fā)送方已經(jīng)知道接收方還可以接收多少數(shù)據(jù),發(fā)送方為什么不把要發(fā)送的數(shù)據(jù)按接收方的最大接收限度一次發(fā)送過去,而是要分成幾個小報文段再發(fā)送呢?

因為數(shù)據(jù)鏈路層規(guī)定單次收發(fā)的有效數(shù)據(jù)不超過一個MTU大小(該值一般為1500字節(jié),可以通過指令 ifconfig 查看),當網(wǎng)絡層發(fā)現(xiàn)上一層交付給它的數(shù)據(jù)較大時,它就會對數(shù)據(jù)進行分片,由接收方網(wǎng)絡層對分好的片進行組裝,而一旦某一個片丟失了,接收端的網(wǎng)絡層就認為整個數(shù)據(jù)段都丟失了,發(fā)送方就需要重發(fā)所有的片,網(wǎng)絡層分片過多,明顯會增加丟包的概率,導致重發(fā)浪費資源,因此需要在傳輸層提前將數(shù)據(jù)分成小段,避免網(wǎng)絡層過多分片。

流量控制

接收端處理數(shù)據(jù)的速度是有限的,如果發(fā)送端發(fā)的太快,導致接收端的緩沖區(qū)被打滿,這個時候如果發(fā)送端繼續(xù)發(fā)送,就會造成丟包,繼而引起不必要的丟包重傳等等一系列連鎖反應,造成網(wǎng)絡資源浪費,因此 TCP 支持根據(jù)接收端的處理能力,來決定發(fā)送端的發(fā)送速度,這個機制就叫做流量控制(Flow Control)。

接收端將自己可以接收的緩沖區(qū)大小放入 TCP 首部中的 “16位窗口大小” 字段, 通過 ACK 應答通知發(fā)送端(16 位數(shù)字最大表示 65535,實際上,TCP 首部 40 字節(jié)選項中還包含了一個窗口擴大因子 M,實際窗口大小是窗口字段的值左移 M 位),窗口大小字段越大,說明網(wǎng)絡的吞吐量越高,當接收端一旦發(fā)現(xiàn)自己的緩沖區(qū)快滿了,就會將窗口大小設置成一個更小的值通知給發(fā)送端,發(fā)送端接受到這個窗口之后,就會減慢自己的發(fā)送速度,如果接收端緩沖區(qū)滿了,就會將窗口置為 0,這時發(fā)送方不再發(fā)送數(shù)據(jù),但是需要定期發(fā)送一個窗口探測數(shù)據(jù)段,使接收端把窗口大小告訴發(fā)送端,在極端情況下,如果連接長時間無法恢復,發(fā)送方可能會選擇關閉連接。

需要注意,TCP滑動窗口的大小并不總是等于對方接收緩沖區(qū)可以接收數(shù)據(jù)的大小。雖然接收窗口的大小通?;诮邮站彌_區(qū)的大小來設置,但它還會受到其他因素的影響,如應用程序的讀取速度和網(wǎng)絡擁塞狀況等。因此,在實際的網(wǎng)絡通信中,發(fā)送方需要動態(tài)地調(diào)整自己的發(fā)送窗口大小,以適應接收方的接收能力和網(wǎng)絡狀況的變化,因此TCP滑動窗口的大小是一個動態(tài)變化的參數(shù),它反映了接收方當前愿意接收的數(shù)據(jù)量以及網(wǎng)絡擁塞狀況等多個因素的綜合影響。

擁塞控制

如果在剛開始階段就發(fā)送大量的數(shù)據(jù),可能會引發(fā)問題,因為網(wǎng)絡上有很多的計算機,如果當前的網(wǎng)絡狀態(tài)已經(jīng)比較擁堵,在不清楚當前網(wǎng)絡狀態(tài)下,貿(mào)然發(fā)送大量的數(shù)據(jù),很有可能導致網(wǎng)絡狀況雪上加霜,為此TCP 引入慢啟動機制:先發(fā)少量的數(shù)據(jù)探探路,摸清當前的網(wǎng)絡擁堵狀態(tài),再決定按照多大的速度傳輸數(shù)據(jù)。

TCP引入了一個稱為擁塞窗口的概念,其實就是一個數(shù)字,當單次發(fā)送數(shù)據(jù)的大小小于該值時,不會導致網(wǎng)絡阻塞,如果大于該值,則可能會導致網(wǎng)絡堵塞,因此每次發(fā)送數(shù)據(jù)包的時候,將擁塞窗口和接收端主機反饋的窗口大小做比較,取較小的值作為實際發(fā)送的窗口。

我們希望擁塞窗口慢啟動后可以隨傳輸輪次較快增長,但又不至于增長過快,因此我們把擁塞窗口的增長進行分段,先以指數(shù)增長,超過某個慢啟動閾值(ssthresh)后以線性方式增長。

故發(fā)送方開始發(fā)送的時候擁塞窗口大小為 1,在TCP連接建立之初,慢啟動閾值通常被設置為一個相對較大的值(如接收窗口的大小),在每次超時重發(fā)的時候,慢啟動閾值會變成原來的一半,同時將傳輸輪次置0(相當于擁塞窗口置回1),慢啟動閾值不會無限減小,通常會設定某個最小值,在某些TCP擁塞控制算法(如CUBIC算法)中,當網(wǎng)絡狀況良好且沒有發(fā)生擁塞時,慢啟動閾值可能會隨著時間的推移而逐漸增加。
在這里插入圖片描述
擁塞控制需要在有限的資源和無限的需求之間找到平衡點,歸根結底是TCP協(xié)議想盡可能快的把數(shù)據(jù)傳輸給對方,但是又要避免給網(wǎng)絡造成太大壓力的折中方案。

延遲應答

如果接收數(shù)據(jù)的主機立刻返回ACK應答,這時候返回的窗口可能比較小,也許等一會兒接收方就把數(shù)據(jù)從緩沖區(qū)取走了,那么這時可以返回的滑動窗口的值就比較大。當然了,不是所有的包都可以延遲應答,其是有一定的限制的:

  1. 數(shù)量限制:每隔N個包就應答一次
  2. 時間限制:超過最大延遲時間就應答一次

具體的數(shù)量和超時時間,操作系統(tǒng)不同之間存在差異,一般N取2,延遲時間取200ms,保證理想情況下在該延遲時間內(nèi)應答不會觸發(fā)發(fā)送方的超時重傳機制。

粘包問題

首先要明確,粘包問題中的包是指的應用層的數(shù)據(jù)包,在 TCP 的協(xié)議頭中,沒有如同 UDP 一樣的 “報文長度” 這樣的字段,但是有一個序號這樣的字段,站在傳輸層的角度,TCP 是一個一個報文過來的,按照序號排好序放在接收緩沖區(qū)中,但站在應用層的角度,看到的只是一串連續(xù)的字節(jié)數(shù)據(jù),那么應用層該如何將一連串的字節(jié)數(shù)據(jù)分割成一個個完整的請求呢?

解決思路很簡單,只需要明確兩個請求之間的邊界即可。對于定長的請求,只需要保證每次都按固定大小讀取即可,當然了一般客戶端請求都是變長的,對于變長的包, 可以在包頭的位置,約定一個包總長度的字段,從而就知道了包的結束位置,還可以在包和包之間使用明確的分隔符(例如使用自定義的應用層協(xié)議,只要保證分隔符不和正文沖突即可)。
對于 UDP 其不存在粘包問題,因為如果還沒有上層交付數(shù)據(jù),UDP 是有報文長度的,有很明確的數(shù)據(jù)邊界,如果站在應用層的站在應用層的角度,使用 UDP 的時候要么收到完整的 UDP 報文,要么不收,不會出現(xiàn)“半個”的情況。

TCP全連接隊列

在建立TCP連接時,如果三次握手已經(jīng)完成,但服務器正處于繁忙狀態(tài)無暇立即調(diào)用accetp獲取連接,此時該連接就會被放到TCP全連接隊列當中,而全連接隊列的長度是有限的,由服務端開啟listen監(jiān)聽時的第二個參數(shù)backlog決定,全連接隊列的長度為 backlog+1 ,如果全連接隊列已經(jīng)滿了,服務器依舊無暇將連接獲取走,此時又來了一個新連接,當前連接就無法進入 established 狀態(tài),則該連接就會失敗。需要注意的是,所有的連接都會先被放到全連接隊列中,再由上層調(diào)用accept將該連接獲取走,因此全連接隊列的長度不宜過小,否則服務端一忙,只有幾個連接可以被放到全連接隊列中,剩余的連接都會失敗,這對大部分客戶來說就是一請求服務就失敗,客戶嘗試幾次之后就會放棄請求,此后盡管服務端空閑了,但客戶可能已經(jīng)沒有請求服務的意愿了,從而增加服務器的閑置率,但全連接隊列的長度也不宜過長,如果隊列很長,處于隊列后面的連接需要很久才可以被accept,可客戶不會等這么久,大部分等了一會就直接斷開連接了,但這期間為了維護這些連接服務端是花費了資源的,例如內(nèi)存、cpu等。

其實Linux 內(nèi)核協(xié)議棧為一個TCP連接管理使用兩個隊列:

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

一些不完整的連接會被放到半連接隊列中,連接完整后就放到全連接隊列中等待上層accept。

下面我們從內(nèi)核角度簡單理解一下全連接隊列:
在這里插入圖片描述
當新建立一個連接時,就是創(chuàng)建了一個struct tcp_sock(它不需要理會自己的全連接隊列)對象,并將該對象放到Listen套接字的全連接隊列中,當上層調(diào)用accept時,則就分配一個文件描述符,把該連接對象放到該文件描述符下面的struct socket中,最后將該文件描述符返回上層,上層只需要對文件描述符操作,就可以進行數(shù)據(jù)收發(fā)了。
在這里插入圖片描述

tcpdump抓包

我們可以使用tcpdump這個工具可以對網(wǎng)絡進行抓包(包括UDP報文),在使用時需要root用戶權限。
在Ubuntu中,安裝指令如下:

sudo apt-get update
sudo apt-get install tcpdump

在 Red Hat 或 CentOS 系統(tǒng)中,可以使用以下命令安裝:

sudo yum install tcpdump

常見的使用方法如下:
使用 tcpdump 的時候,有些主機名會被云服務器解釋成為隨機的主機名,如果不想要,就添加 -n 選項

  1. 捕獲所有網(wǎng)絡接口上的 TCP 報文
sudo tcpdump -i any tcp

-i any 字段指定捕獲所有網(wǎng)絡接口上的數(shù)據(jù)包,i 可以理解成為 interface 的意思,tcp 字段指定捕獲 TCP 協(xié)議的數(shù)據(jù)包。

  1. 捕獲特定源或目的 IP 地址的 TCP 報文

使用 host 關鍵字可以指定源或目的 IP 地址,同時可以使用 and 關鍵字連接兩個條件

//捕獲源 IP 地址為192.168.1.100 的 TCP 報文
sudo tcpdump src host 192.168.1.100 and tcp
//捕獲目的 IP 地址為 192.168.1.200 的 TCP 報文
sudo tcpdump dst host 192.168.1.200 and tcp
//同時指定源和目的 IP 地址
sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp
  1. 捕獲特定端口的 TCP 報文

使用 port 關鍵字可以指定端口號

//捕獲端口號為 80 的 TCP 報文
sudo tcpdump port 80 and tcp
  1. 保存捕獲的數(shù)據(jù)包到文件

使用 -w 選項可以將捕獲的數(shù)據(jù)包保存到文件中,以便后續(xù)分析

sudo tcpdump -i eth0 port 80 -w data.pcap

把捕獲到的 HTTP 流量保存到名為 data.pcap 的文件中,pcap 后綴的文件通常與 PCAP(Packet Capture)文件格式相關,是一種用于捕獲網(wǎng)絡數(shù)據(jù)包的文件格式。

  1. 從文件中讀取數(shù)據(jù)包進行分析

使用 -r 選項可以從文件中讀取數(shù)據(jù)包進行分析

tcpdump -r data.pcap
  1. 捕獲指定網(wǎng)絡接口上的 TCP 報文
//捕獲某個特定網(wǎng)絡接口(如 eth0)上的 TCP 報文
sudo tcpdump -i eth0 tcp

wireshark 是 windows 下的一個網(wǎng)絡抓包工具,雖然 Linux 命令行中有 tcpdump 工具同樣能完成抓包,但是 tcpdump 是純命令行界面,使用起來不如 wireshark 方便,如果需要可以在wireshark下載下載 wireshark,具體使用教程自行網(wǎng)上參考。

在TCP小節(jié)的最后,我們考慮一些TCP異常情況:

  1. 進程終止:進程終止會釋放文件描述符,仍然可以發(fā)送 FIN,和正常關閉沒有什么區(qū)別
  2. 機器重啟: 和進程終止的情況相同
  3. 機器掉電/網(wǎng)線斷開:其中一端一開始不會意識到對方出現(xiàn)問題,會認為連接還在,一旦該端有寫入操作,就會發(fā)現(xiàn)連接已經(jīng)不在了,就會發(fā)送RST報文段來異常關閉連接(與TCP的正常關閉過程不同,發(fā)送RST報文段關閉連接時,不需要等待緩沖區(qū)的包都發(fā)送出去,也不需要接收端發(fā)送ACK報文段來確認),即使沒有寫入操作,TCP 自己也內(nèi)置了一個?;疃〞r器(一般為2個小時,沒有結合到應用層具體的服務,因此顯得不合理),如果雙方在保活定時器設置的時間沒有數(shù)據(jù)交換,TCP就會認為連接可能已經(jīng)不再有效或對方可能已經(jīng)異常,TCP會發(fā)送一個探查報文段(也稱為?;钐綔y報文)給對方,以檢查對方是否仍然在線并能夠響應,如果對方在線并正常響應了這個探查報文段,那么TCP會收到回應,并重新復位?;疃〞r器,以繼續(xù)維持連接的有效性。如果對方?jīng)]有響應這個探查報文段,TCP可能會繼續(xù)發(fā)送多個探查報文段(具體的次數(shù)和間隔時間取決于TCP的實現(xiàn)和配置),如果在發(fā)送完所有探查報文段后仍未收到任何響應,TCP就會認為連接已經(jīng)失效,并主動釋放這個TCP連接。

另外,應用層的某些協(xié)議,也有一些這樣的檢測機制,例如 HTTP 長連接中,也會定期檢測對方的狀態(tài),例如使用 QQ時,在 QQ 斷線之后,也會定期嘗試重新連接。

總之,TCP 之所以這么復雜就是因為其要保證可靠性,同時又盡可能的提高性能。
通過以下機制提供可靠性:

  • 校驗和
  • 序列號(按序到達)
  • 確認應答
  • 超時重發(fā)
  • 連接管理
  • 流量控制
  • 擁塞控制

通過以下機制提高性能:

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

基于 TCP 應用層協(xié)議:

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

網(wǎng)絡層

網(wǎng)絡層的主要作用為根據(jù)目標主機的IP在網(wǎng)絡中進行路由選擇,即尋找一條合適的傳輸路徑,其有很大概率將報文轉運到目標主機,配合上TCP的重發(fā)策略就可以保證數(shù)據(jù)傳輸?shù)目煽啃浴?/p>

協(xié)議格式

在這里插入圖片描述
4 位版本號(version):指定 IP 協(xié)議的版本,對于 IPv4 來說是 4,對于 IPv6 來說是 6

4 位頭部長度(header length):IP 頭部的長度是多少個 32bit, 也就是 4*length 個字節(jié),其中l(wèi)ength(4bit)表示最大的數(shù)字是 15,因此 IP 報頭最大長度是 60 字節(jié)

8 位服務類型(Type Of Service):3 位優(yōu)先權字段(已經(jīng)棄用),4 位 TOS 字段,1 位保留字段(必須置為 0),以下是文心一言對這些字段的解釋:
在這里插入圖片描述

16 位總長度(total length):整個IP 數(shù)據(jù)報的長度,以字節(jié)為單位

16 位標識(id):用于唯一的標識主機發(fā)送的報文,確保了每個IP數(shù)據(jù)報在網(wǎng)絡中的唯一性,從而避免了混淆和沖突。如果傳輸層交付下來的報文被網(wǎng)絡層分片了,那么每一個片里面的這個 id 都是相同的,此時還可以確保接收端能夠準確地識別出哪些分片屬于同一個原始數(shù)據(jù)報。

3 位標志字段:第一位保留(現(xiàn)在不用,以后可能要用到);第二位(DF(Don’t Fragment)位)置為 1 表示禁止分片,這時候如果報文長度超過 MTU,該報文就會被丟棄,并向發(fā)送方發(fā)送一個ICMP消息通知;第三位(MF(More Fragment)位)用于指示當前數(shù)據(jù)報是否是分片數(shù)據(jù)報序列中的最后一個分片,如果是最后一個分片則置0,否則置1,以幫助接收端正確地重組分片數(shù)據(jù)報,從而恢復原始的IP數(shù)據(jù)報

13 位分片偏移(framegament offset):是分片相對于原始 IP 報文開始處的偏移,其實就是在表示當前分片在原報文中處在哪個位置,實際偏移的字節(jié)數(shù)是這個值乘8得到的. 因此,除了最后一個報文之外,其他報文的長度必須是 8 的整數(shù)倍(否則報文就不連續(xù)了)

8 位生存時間(Time To Live, TTL):數(shù)據(jù)報到達目的地的最大報文跳數(shù),該值一般是64. 每次經(jīng)過一個路由, TTL 就減1,如果一直減到 0 還沒到達目標主機,那么就直接丟棄該報文,這個字段主要是用來防止出現(xiàn)路由循環(huán)。

8 位協(xié)議:表示上層協(xié)議的類型

16 位頭部校驗和:使用 CRC 進行校驗,來鑒別頭部是否損壞,數(shù)據(jù)部分由上層傳輸層檢查,以減少計算量

32 位源地址和 32 位目標地址:表示發(fā)送端和接收端的IP地址

選項字段(不定長, 最多 40 字節(jié)):用于提供了額外的功能,允許發(fā)送方在IP數(shù)據(jù)報中嵌入一些可選的信息

IP報文的分片和組裝

數(shù)據(jù)鏈路層規(guī)定網(wǎng)絡層交付給它的單個報文長度不能超過MTU(Maximum Transmission Unit,該值一般是1500字節(jié)),除去IP報頭的20字節(jié),網(wǎng)絡層單次接收的傳輸層交付給它的報文長度如果超過1480字節(jié),網(wǎng)絡層就要對報文進行分片,再由接收方的網(wǎng)絡層進行組裝,但如果其中任何一個分片丟失了,接收方的網(wǎng)絡層就無法組裝這個報文,自然不會將報文交付傳輸層,最后發(fā)送方只能進行重傳。因此網(wǎng)絡層分片會增加丟包重傳的概率,不建議讓網(wǎng)絡層進行分片,一般是在傳輸層就控制好報文的長度。
在這里插入圖片描述

  1. 分片
    網(wǎng)絡層分片時
  • 每一個分片都要有自己的IP報頭,報頭中的16位標識符填充的是相同的值,這樣接收方網(wǎng)絡層就知道哪些分片是屬于同一個報文。
  • 每一個分片的3位標志位的MF標志位置1,但最后一個分片的MF標志位置0,這樣接收方網(wǎng)絡層就知道哪個分片是在報文末尾
  • 每一個分片的13位分片偏移填充該分片的有效載荷在原始報文中的偏移量除以8,因此每一個分片的有效載荷的長度必須都是8的整數(shù)倍(最后一個分片除外),這樣做是因為13位分片偏移字段只有13位,而IP報文的總長度有16位,偏移量除以8 就可以保證13位分片偏移可以覆蓋整個報文。
  1. 組裝
    接收方網(wǎng)絡層接收到報文后,只需要判斷MF標志位為1或13片偏移不為0就可以知道當前報文是一個分片,需要進行組裝。
    組裝時只需要按照片偏移對所有的片從小到大進行排序,如果找不到片偏移為0的片,就可以確定原始報文的第一片丟了,如果最后一片的MF標志位不為0,就可以確定原始報文的最后一片丟了,如果前一片的13位偏移加上前一片的有效載荷除以8不等于當前片的13位偏移就可以確定中間的某一片丟失了。如果確認沒有片丟失,就可以直接組裝排好序的片交付上層了。

網(wǎng)段劃分

IP 地址分為兩個部分,網(wǎng)絡號和主機號

  • 網(wǎng)絡號:保證相互連接的兩個網(wǎng)段具有不同的標識
  • 主機號:同一網(wǎng)段內(nèi),主機之間具有相同的網(wǎng)絡號,但是必須有不同的主機號

不同的子網(wǎng)其實就是把網(wǎng)絡號相同的主機放到一起,如果在子網(wǎng)中新增一臺主機,則這臺主機的網(wǎng)絡號和這個子網(wǎng)的網(wǎng)絡號一致,但是主機號必須不能和子網(wǎng)中的其他主機重復,因此通過合理設置主機號和網(wǎng)絡號就可以保證在相互連接的網(wǎng)絡中,每臺主機的 IP 地址都不相同。但手動管理子網(wǎng)內(nèi)的 IP是一個相當麻煩的事情,為此產(chǎn)生了有一種叫做 DHCP的技術,能夠自動的給子網(wǎng)內(nèi)新增主機節(jié)點分配 IP 地址,避免了手動管理 IP 的不便,一般的路由器都帶有 DHCP 功能,因此路由器也可以看做一個 DHCP 服務器。

那么問題來了,IP地址只有32個比特位,給網(wǎng)絡號和主機號分配多少個比特位才是合適的呢,即怎么進行網(wǎng)段劃分呢?

常見的網(wǎng)段劃分有靜態(tài)劃分、子網(wǎng)劃分等

  1. 靜態(tài)劃分

過去曾經(jīng)提出一種劃分網(wǎng)絡號和主機號的方案,把所有 IP 地址分為五類:
在這里插入圖片描述

  • A 類 0.0.0.0 到 127.255.255.255
  • B 類 128.0.0.0 到 191.255.255.255
  • C 類 192.0.0.0 到 223.255.255.255
  • D 類 224.0.0.0 到 239.255.255.255
  • E 類 240.0.0.0 到 247.255.255.255

隨著 Internet 的飛速發(fā)展,這種劃分方案的局限性很快顯現(xiàn)出來,大多數(shù)組織都申請 B 類網(wǎng)絡地址,導致 B 類地址很快就分配完了,而 A 類卻浪費了大量地址,且申請了一個 B 類地址,理論上一個子網(wǎng)內(nèi)能允許 6 萬 5 千多個主機(A 類地址的子網(wǎng)內(nèi)的主機數(shù)更多),然而實際網(wǎng)絡架設中,不會存在一個子網(wǎng)內(nèi)有這么多的情況. 因此大量的 IP 地址都被浪費掉了。

  1. 無分類編址CIDR

針對以上情況后面提出了新的劃分方案,稱為 CIDR(Classless Interdomain Routing)。
引入一個額外的子網(wǎng)掩碼(subnet mask)來區(qū)分網(wǎng)絡號和主機號,子網(wǎng)掩碼也是一個 32 位的正整數(shù),在二進制中0和1不允許交替出現(xiàn)(即連續(xù)的1之后必須是連續(xù)的0,其中0或1的數(shù)量可以為0),將 IP 地址和子網(wǎng)掩碼進行“按位與”操作,得到的結果就是網(wǎng)絡號,而網(wǎng)絡號和主機號的劃分與這個 IP 地址是 A 類、B 類還是 C 類無關,例如:
在這里插入圖片描述
可見,只要對IP 地址與子網(wǎng)掩碼做與運算可以得到網(wǎng)絡號,主機號從全 0 到全 1 就是子網(wǎng)的地址范圍。IP 地址和子網(wǎng)掩碼還有一種更簡潔的表示方法,例如 140.252.20.68/2,表示 IP 地址為140.252.20.68, 子網(wǎng)掩碼的高 24 位是 1,也就是 255.255.255.0。

這樣,通過增縮掩碼的寬度(即增減掩碼中1的數(shù)量)就可以控制該網(wǎng)段中的IP的數(shù)量,以合理分配IP。

由此出現(xiàn)了一些特殊的IP地址:

  • 將 IP 地址中的主機地址全部設為 0就成為了網(wǎng)絡號,代表這個局域網(wǎng),用于標識一個特定的網(wǎng)絡或子網(wǎng)
  • 將 IP 地址中的主機地址全部設為 1就成為了廣播地址,用于給同一個鏈路中相互連接的所有主機發(fā)送數(shù)據(jù)包
  • 127.*的 IP 地址用于本機環(huán)回(loop back)測試,通常是 127.0.0.1

主機號為全0或全1的IP地址有特殊用途,不能分配給主機使用。

網(wǎng)絡路由

路由在復雜的網(wǎng)絡結構中,找出一條通往終點的路線,在了解路由之前,我們先了解一下私有IP和公網(wǎng)IP。我們將IP地址分為私有IP和公網(wǎng)IP,并允許私有IP在不同網(wǎng)段內(nèi)可以重復,但不允許在同一個網(wǎng)段中出現(xiàn)相同的私有IP,而公網(wǎng)IP在網(wǎng)絡中必須是唯一的,同時也不允許在公網(wǎng)中出現(xiàn)私有IP。

RFC 1918 規(guī)定了以下類型的IP地址為私有 IP 地址:

  • 10.*,共 16,777,216 個地址
  • 172.16.*到 172.31.*,共 1,048,576 個地址
  • 192.168.*,共 65,536 個地址

包含在這個范圍中的都是私有 IP,其余的則稱為全局 IP(或公網(wǎng) IP),原則上處于不同的子網(wǎng)中的2臺私有IP的主機不能直接通信,必須通過公網(wǎng)上的服務器進行轉發(fā)(例如QQ的服務器)。

我們的網(wǎng)路世界中的主機之間并不是一張任意連接的圖,這些主機是有一定的層次結構的(類似于國家、省、市、鎮(zhèn)、鄉(xiāng)一樣的層次結構),一般是由運營商進行合理規(guī)劃:
在這里插入圖片描述
一個路由器一般配置有兩個 IP 地址(更通常的情況是擁有一個WAN口和多個LAN口),一個是WAN(Wide Area Network)口IP,一個是LAN(Local Area Network)口IP(也稱為子網(wǎng)IP,路由器的子網(wǎng)IP其實都是一樣的,通常都是192.168.1.1),其中路由器LAN 口IP與其下一級的路由器的WAN口IP或普通主機的IP處于同一網(wǎng)段中(相當于它們在同一個局域網(wǎng)中,之間可以互相通信),WAN口IP與上一級路由器的LAN口IP相連或該WAN口IP直接就是一個公網(wǎng)IP,這樣路由器就連接了2個不同的子網(wǎng)。每一個家用路由器,其實都是運營商路由器的子網(wǎng)中的一個節(jié)點,而運營商路由器可能會有很多級,最外層的運營商路由器的WAN 口 IP 就是一個公網(wǎng) IP。

子網(wǎng)內(nèi)的主機需要和外網(wǎng)進行通信時,路由器將 IP 首部中的 IP 地址進行替換(替換成 WAN 口 IP),這樣逐級替換,最終數(shù)據(jù)包中的 IP 地址成為一個公網(wǎng) IP。這種技術稱為 NAT(Network Address Translation,網(wǎng)絡地址轉換)技術,這樣公網(wǎng)中就不會出現(xiàn)私有IP了,因此如果希望我們自己實現(xiàn)的服務器程序能夠在公網(wǎng)上被訪問到,就需要把程序部署在一臺具有公網(wǎng)IP 的服務器上,而這樣的服務器可以在阿里云或騰訊云上進行購買。

路由的過程就是不斷地問路,一跳一跳(Hop by Hop)的往下一站走:當 IP 數(shù)據(jù)包到達路由器時,路由器會先查看目的 IP以決定這個數(shù)據(jù)包是直接發(fā)送給目標主機還是需要發(fā)送給下一個路由器,如此反復,一直到達目標 IP 地址為止。

那么路由器是怎么知道數(shù)據(jù)包是直接發(fā)送給目標主機還是需要發(fā)送給下一個路由器呢?

路由器自己內(nèi)部會維護一張路由表,類似于下面的表,可以使用指令 route 查看:
在這里插入圖片描述
Destination:目的網(wǎng)絡地址
Gateway:下一跳地址,為*意味著目標主機與本機在同一子網(wǎng)中可以直接訪問
Genmask:子網(wǎng)掩碼
Flags:U 標志表示此條目有效(可以禁用某些條目),G標志表示此條目的下一跳地址是某個路由器的地址,沒有 G 標志的條目表示目的網(wǎng)絡地址是與本機接口直接相連的網(wǎng)絡,不必經(jīng)路由器轉發(fā)。
Metric:用以確定到達目的地的最佳路徑的計量標準,用于在多個可能的路由中選擇最佳路徑,根據(jù)Metric值來判斷哪條路徑是最優(yōu)的
Ref:指示有多少其他路由項引用了這個路由項
Use:表示該路由項被路由軟件查找或使用的次數(shù)
Iface:發(fā)送接口

當路由器得到一個報文后,它會用報文中的目的IP地址與子網(wǎng)掩碼Genmask做與運算,如果結果與Destination一致,就通過其對應的發(fā)送接口將數(shù)據(jù)發(fā)送出去,否則繼續(xù)往下匹配,如果路由表中其它行都不匹配時,就按缺省路由條目規(guī)定的接口發(fā)送到下一跳地址。

當路由器新增一個LAN口子網(wǎng)或WAN口子網(wǎng)時,路由表會為該子網(wǎng)生成一個新增一條記錄,路由表可以由網(wǎng)絡管理員手動維護(靜態(tài)路由),也可以通過一些算法自動生成(動態(tài)路由),例如距離向量算法、LS 算法、Dijkstra 算法等。

現(xiàn)在我們更加具體的看看數(shù)據(jù)包路由的過程:
現(xiàn)在手機的數(shù)據(jù)鏈路層有一個數(shù)據(jù)包,目標主機是公網(wǎng)中的京東的服務器,該數(shù)據(jù)會先被發(fā)送到與手機相連的路由器中,路由器拿到該數(shù)據(jù)包后,取出目的IP地址,根據(jù)路由表決定向上層路由器交付還是交付到子網(wǎng)中的某個主機,如此層層交付,到達服務商的公網(wǎng)出口路由器,此時數(shù)據(jù)包的源IP地址會被替換層該路由器的WAN口IP地址,繼續(xù)向上交付直至目標主機,目標主機應答時,根據(jù)數(shù)據(jù)包的源IP地址就可以將數(shù)據(jù)返回到請求時的服務商的公網(wǎng)出口路由器,通過NAT技術(后面講)就可以把數(shù)據(jù)返回到手機上。

數(shù)據(jù)鏈路層

現(xiàn)在我們已經(jīng)知道在復雜的網(wǎng)絡結構中是如何找出一條通往終點的路線的了,那么數(shù)據(jù)是如何在線路中兩個節(jié)點進行轉發(fā)的呢?

以太網(wǎng)是當前應用最廣泛的局域網(wǎng)技術,它不是一種具體的網(wǎng)絡,而是一種技術標準,既包含了數(shù)據(jù)鏈路層的內(nèi)容,也包含了一些物理層的內(nèi)容,例如它規(guī)定了網(wǎng)絡拓撲結構、訪問控制方式、傳輸速率等,以及以太網(wǎng)中的網(wǎng)線必須使用雙絞線,和以太網(wǎng)并列的還有令牌環(huán)網(wǎng)、無線LAN 等。

以太網(wǎng)幀格式

在這里插入圖片描述
源地址和目的地址:當前主機網(wǎng)卡和下一跳主機網(wǎng)卡的硬件地址(也叫 MAC 地址),該地址的長度是 48 位,是在網(wǎng)卡出廠時固化的

類型:幀協(xié)議類型字段有三種值,分別對應 IP(Internet Protocol)、ARP(Address Resolution Protocol)、RARP(Reverse Address Resolution Protocol)

  • IP:表示該幀封裝的是IP數(shù)據(jù)包
  • ARP:表示該幀封裝的是ARP數(shù)據(jù)包,用于將IP地址解析為對應的MAC地址。ARP數(shù)據(jù)包包括ARP請求包和ARP應答包兩種類型,分別用于發(fā)送查詢請求和接收應答回復。
  • RARP:表示該幀封裝的是RARP數(shù)據(jù)包,RARP協(xié)議是ARP協(xié)議的逆向過程,用于將MAC地址解析為對應的IP地址,現(xiàn)在RARP協(xié)議已經(jīng)逐漸被DHCP等更先進的協(xié)議所取代,因此在現(xiàn)代網(wǎng)絡中遇到RARP數(shù)據(jù)包的可能性較低

CRC:循環(huán)冗余校驗(Cyclic Redundancy Check),用于檢驗整個數(shù)據(jù)幀是否傳輸出錯

MTU

以太網(wǎng)規(guī)定以太網(wǎng)幀中的數(shù)據(jù)長度(即有效載荷,不包括幀頭)最小為46字節(jié),最大為1500字節(jié),ARP 數(shù)據(jù)包的長度不夠46字節(jié)的,要在后面補填充位,這個最大值1500稱為以太網(wǎng)的最大傳輸單元(MTU,Maximum Transmission Unit),不同的網(wǎng)絡類型有不同的MTU,可以用指令 ifconfig 查看MTU、MAC地址和IP地址。如果鏈路層接收到一個超過MTU限制的報文時,它通常會通知網(wǎng)絡層該報文的大小超過了MTU限制,并請求網(wǎng)絡層對報文進行分片處理。

  1. MTU 對 IP 協(xié)議的影響:

由于數(shù)據(jù)鏈路層 MTU 的限制,對于較大的 IP 數(shù)據(jù)包要進行分包,即將較大的 IP 包分成多個小包,并給每個小包打上標簽,到達對端時再將這些小包會按順序重組拼裝到一起返回給傳輸層,一旦這些小包中任意一個小包丟失,接收端的重組就會失敗,但是 IP 層不會負責重新傳輸數(shù)據(jù),而是由傳輸層進行重發(fā)。

  1. MTU 對 UDP 協(xié)議的影響:

一旦 UDP 攜帶的數(shù)據(jù)超過 1472(1500 - 20(IP 首部) - 8(UDP 首部)),那么就會在網(wǎng)絡層分成多個 IP 數(shù)據(jù)報,這多個 IP 數(shù)據(jù)報有任意一個丟失,都會引起接收端網(wǎng)絡層重組失敗,整個報文就丟失了,因此如果 UDP 數(shù)據(jù)報在網(wǎng)絡層被分片整個數(shù)據(jù)被丟失的概率會大大增加。

  1. MTU 對于 TCP 協(xié)議的影響

TCP 的一個數(shù)據(jù)報也受制于 MTU,單個TCP數(shù)據(jù)報的最大消息長度,稱為 MSS(Max Segment Size)
在這里插入圖片描述

通信雙方在三次握手發(fā)送 SYN 報文的時會進行 MSS 協(xié)商,以確保數(shù)據(jù)傳輸?shù)捻樌M行。最理想的情況下, MSS 的值正好是在網(wǎng)絡層不會被分片處理的最大長度,雙方在 TCP 頭部寫入自己能支持的 MSS 值,然后雙方得知對方的 MSS 值之后,選擇較小的作為最終 MSS,而MSS 的值就是在 TCP 首部的 40 字節(jié)變長選項中(kind=2)。

ARP協(xié)議

在鏈路的2個節(jié)點網(wǎng)絡通信時時,數(shù)據(jù)包首先是被網(wǎng)卡接收到再到上層協(xié)議的,如果接收到的數(shù)據(jù)幀的硬件地址與本機不符,則直接丟棄該數(shù)據(jù)幀,因此在通訊前必須獲得目的主機的硬件地址(MAC地址)。

ARP(Address Resolution Protocol,地址解析協(xié)議) 不是一個單純的數(shù)據(jù)鏈路層的協(xié)議,而是一個介于數(shù)據(jù)鏈路層和網(wǎng)絡層之間的協(xié)議,它建立了主機 IP 地址 和 MAC 地址 的映射關系。
在這里插入圖片描述

ARP協(xié)議格式

ARP協(xié)議的格式如下:
在這里插入圖片描述

硬件類型:數(shù)據(jù)鏈路層的網(wǎng)絡類型,1 為以太網(wǎng);
協(xié)議類型:要轉換的地址類型,0x0800 為 IP 地址
硬件地址長度:對于以太網(wǎng)地址為 6 字節(jié)
協(xié)議地址長度:對于 IP 地址為 4 字節(jié)
op:字段為 1 表示 ARP 請求,op 字段為 2 表示 ARP 應答

注意到源 MAC 地址、目的 MAC 地址在以太網(wǎng)首部和 ARP 請求中各出現(xiàn)一次,對于鏈路層為以太網(wǎng)的情況是多余的,但如果鏈路層是其它類型的網(wǎng)絡則有可能是必要的。

當前主機獲取下一個節(jié)點的MAC地址的過程為:

源主機發(fā)出 ARP 請求,在以太網(wǎng)幀首部的以太網(wǎng)目的地址填充FF:FF:FF:FF:FF:FF,表示廣播,這個請求會被廣播到本地網(wǎng)段,以詢問IP地址是下一跳主機的硬件地址是多少,目的主機接收到廣播的 ARP 請求,會先查看OP字段以判斷該報文是ARP請求還是ARP應答,接著發(fā)現(xiàn)其中的目的 IP 地址與本機相符,它會將自己的硬件地址填寫在應答包中,然后將該ARP應答數(shù)據(jù)包給源主機,這樣源主機就獲取了下一跳的MAC地址。

為了網(wǎng)絡性能,每臺主機都維護一個 ARP 緩存表,可以用指令 arp -a 查看,緩存表中的表項是有過期時間的(一般為 20 分鐘),如果 20 分鐘內(nèi)沒有再次使用某個表項,則該表項失效,下次需要的話要發(fā) ARP 請求來獲得目的主機的硬件地址。

ARP欺騙

正常情況下網(wǎng)絡通信是這樣的:
在這里插入圖片描述
主機A在自己的ARP緩存表中記錄IP地址ipR和macR的映射關系,如果有報文的下一跳IP地址是ipR,直接向macR主機發(fā)送即可;同時路由器R在自己的ARP緩存表中記錄IP地址ipA和macA的映射關系,如果有報文的下一跳IP地址是ipA,直接向macA主機發(fā)送即可。

但此時來了一個中間人主機M,IP地址是ipM,MAC地址是macM,它大量向主機A發(fā)送ARP應答報文,告訴主機A現(xiàn)在ipR對應的MAC地址是macM,主機A對此不知情,就對自己的ARP緩存表進行了更新,同理,中間人M對路由器R進行相同的操作,R就認為ipA對應的MAC地址是macM:
在這里插入圖片描述
這樣主機A發(fā)往路由器R和路由器R返回給主機A的報文都要經(jīng)過中間人M,M就成功竊取到它們的通信數(shù)據(jù):在這里插入圖片描述

局域網(wǎng)轉發(fā)

同一個局域網(wǎng)中的主機是可以直接進行通信的:當一臺主機發(fā)送數(shù)據(jù)幀時,該局域網(wǎng)的所有主機都可以接收到,但這些主機會拿得到的數(shù)據(jù)幀的幀頭的MAC地址與自己的MAC地址做比對,以決定是忽略還是接收這個數(shù)據(jù)幀,最終只有MAC地址匹配的主機會選擇接收這個數(shù)據(jù)幀。

在局域網(wǎng)中是不允許多臺主機同時發(fā)送消息的,任何時候都只允許一臺主機向局域網(wǎng)中發(fā)送數(shù)據(jù)幀(相當于局域網(wǎng)就是一個臨界資源),否則就會發(fā)生數(shù)據(jù)碰撞,導致數(shù)據(jù)混淆無法區(qū)分。不同網(wǎng)絡對該問題的解決方案不同,以以太網(wǎng)為例,以太網(wǎng)允許數(shù)據(jù)碰撞的發(fā)生,發(fā)送數(shù)據(jù)的主機會檢測是否發(fā)送了碰撞,如果檢測到了數(shù)據(jù)碰撞,當前主機就會暫停數(shù)據(jù)的發(fā)送,等待一個合適的時間之后重新發(fā)送數(shù)據(jù)幀。相比之下,令牌環(huán)網(wǎng)的解決方案就比較簡單:它為當前的局域網(wǎng)分配一個令牌,只有持有令牌的主機才可以向局域網(wǎng)中發(fā)送數(shù)據(jù),這類似于一個互斥鎖。

至于數(shù)據(jù)幀是如何發(fā)送到局域網(wǎng)中,以什么信號形式怎么傳輸?shù)侥繕酥鳈C,目標主機又是如何將這些信號識別成一個數(shù)據(jù)幀的等等問題,這些都是物理層的內(nèi)容,本文不做討論。

其他協(xié)議和技術

NAT技術

我們知道,IP 地址(IPv4)是一個 4 字節(jié) 32 位的正整數(shù),一共只有 2的32 次方 個 IP地址,大概是 43 億左右,而 TCP/IP 協(xié)議規(guī)定,每個主機都需要有一個 IP 地址,難道只有 43 億臺主機能接入網(wǎng)絡嗎?而且實際上,由于一些特殊的 IP 地址的存在數(shù)量遠不足 43 億,另外 IP 地址也并非是按照主機臺數(shù)來配置的,而是每一個網(wǎng)卡都需要配置一個或多個 IP 地址。盡管CIDR 在一定程度上緩解了 IP 地址不夠用的問題(提高了利用率, 減少了浪費,但是 IP地址的絕對上限并沒有增加), IP地址仍然不是很夠用,目前時候有三種方式來解決該問題:

  1. 動態(tài)分配 IP 地址:只給接入網(wǎng)絡的設備分配 IP 地址,因此同一個 MAC 地址的設備,每次接入互聯(lián)網(wǎng)中得到的 IP 地址不一定是相同的,這種方法任然只是緩解了該問題,沒有從根本上解決問題。

  2. IPv6:IPv6 并不是 IPv4 的簡單升級版,它們是互不相干的兩個協(xié)議,彼此并不兼容,IPv6 用 16 字節(jié) 128 位來表示一個 IP 地址,該方法幾乎從根本上解決了問題,但遺憾的是目前 IPv6 還沒有普及。

  3. NAT (Network Address Translation,網(wǎng)絡地址轉換)技術

在路由器內(nèi)部,有一張路由器自動生成的用于地址轉換的表,當數(shù)據(jù)包到達某個路由器后,如果是源主機第一次向目的主機發(fā)送數(shù)據(jù),該表就會新增一條記錄,記錄的是數(shù)據(jù)包的源IP地址和目的IP地址的映射關系,接著數(shù)據(jù)包的源IP地址會被替換成當前路由器的WAN口IP,如此層層替換,直到數(shù)據(jù)包的源IP地址被替換成一個公網(wǎng)IP(運營商公網(wǎng)出口路由器的WAN口IP)。這樣當數(shù)據(jù)包返回時,利用該表就可以溯源上一個主機,層層溯源就可以將數(shù)據(jù)包返回到原始發(fā)送數(shù)據(jù)的主機。
在這里插入圖片描述
NAT 路由器將源地址從 10.0.0.10 替換成全局的 IP 202.244.174.37,當服務器的數(shù)據(jù)包返回到路由器時,它會把目標 IP 從 202.244.174.37 替換回10.0.0.10,這樣數(shù)據(jù)包就成功返回到原始發(fā)送數(shù)據(jù)的主機了。

如果局域網(wǎng)內(nèi),有多個主機都訪問同一個外網(wǎng)服務器,那么對于服務器返回的數(shù)據(jù)中,目的 IP 都是相同的,那么 NAT 路由器如何判定將這個數(shù)據(jù)包轉發(fā)給哪個局域網(wǎng)的主機?
這時候 NAPT (Network Address Port Translation,網(wǎng)絡地址端口轉換,是NAT的變體)來解決這個問題,它使用 IP+port 來建立這個映射關系。
在這里插入圖片描述
當源主機第一次向目的主機發(fā)送數(shù)據(jù)時,它會拿數(shù)據(jù)包中的源IP地址、源端口號與該路由器的WAN口IP地址與新端口號(該新端口號路由器自行分配,但保證唯一性)做映射生成一條新紀錄添加到轉換表中,接著把數(shù)據(jù)包的源IP地址和端口號替換成路由器的WAN口IP地址與新端口號。這樣數(shù)據(jù)包返回時就可以找到原始發(fā)送數(shù)據(jù)的主機。

例如有下面這樣一個網(wǎng)絡:
在這里插入圖片描述

M和B在一個子網(wǎng)中,N和C在一個子網(wǎng)中,三個路由器A、B、C在同一個子網(wǎng)中,其路由器A的WAN口IP是一個公網(wǎng)IP,服務器處于公網(wǎng)中,其端口號為11,M和N的私有IP相同,它們的端口號也相同都是7,現(xiàn)在他們都向服務器S發(fā)起請求,M發(fā)出的報文會經(jīng)過B到A最后抵達S,N發(fā)出的報文會經(jīng)過C到A最后抵達S,他們建立的轉換表大致如下:

在這里插入圖片描述
這種映射關系也是由路由器自動維護的,例如在 TCP 的情況下,建立連接時就會生成這個表項,在斷開連接后就會刪除這個表項。

由于NAT依賴轉換表,所以它存在一定的缺陷:

  • 無法從NAT外部向內(nèi)部服務器建立連接
  • 裝換表的生成和銷毀都需要額外開銷
  • 通信過程中一旦NAT設備異常,即使存在熱備(一種備份策略,它在主系統(tǒng)正常運行時,備份系統(tǒng)也處于運行狀態(tài),并且隨時準備接管主系統(tǒng)的工作),所有的TCP連接也都會斷開

通過NAT技術,就允許了私有IP的重復,極大的緩解了IPv4的IP地址不足問題,但公網(wǎng)IP數(shù)量依舊是有限的,因此可以預見IPv6仍將是未來的主流。

DNS

TCP/IP 中使用 IP 地址和端口號來確定網(wǎng)絡上的一臺主機的一個程序,但是 IP 地址不方便記憶,于是人們想到用主機名(即一串字符串)來標識網(wǎng)絡中的IP,并且使用 hosts 文件來描述主機名和 IP 地址的關系。

最初是通過互連網(wǎng)信息中心(SRI-NIC)來管理這個 hosts 文件的,但如果一個新服務主機要接入網(wǎng)絡或者某個服務主機 IP 變更,都需要到信息中心申請變更 hosts 文件,其他計算機也需要定期下載更新新版本的 hosts 文件才能正確上網(wǎng),這樣就顯得十分不方便,于是產(chǎn)生了 DNS 系統(tǒng)。

DNS(Domain Name System,域名系統(tǒng))是一整套從域名映射到 IP 的系統(tǒng),如果新計算機接入網(wǎng)絡則將這個信息注冊到數(shù)據(jù)庫中,用戶輸入域名的時候,會自動查詢 DNS 服務器,由 DNS 服務器檢索數(shù)據(jù)庫,得到對應的 IP 地址。但是至今我們的計算機上仍然保留了 hosts 文件,在域名解析的過程中仍然會優(yōu)先查找hosts 文件的內(nèi)容。一般一個組織的系統(tǒng)管理機構自己會維護系統(tǒng)內(nèi)的每個主機的 IP 和主機名的對應關系。

域名是分級的,各個部分使用 . 連接,例如域名:www.baidu.com

  • com:頂級域名,com 表示這是一個企業(yè)域名,同級的還有 net (網(wǎng)絡提供商),org (非盈利組織) 等
  • baidu:一級域名(或稱為頂級子域),用于標識其在互聯(lián)網(wǎng)上的身份和位置,一般是公司名或者組織名
  • www:World Wide Web,萬維網(wǎng),二級域名,這只是一種習慣用法,之前人們在使用域名時,往往命名成類似于ftp.xxx.xxx或者www.xxx.xxx 這樣的格式,來表示主機支持的協(xié)議

域名結構是樹狀結構:
在這里插入圖片描述

樹的最頂端代表根服務器,根的下一層就是由我們所熟知的.com、.net、.cn等通用域和.cn、.uk等國家域組成,被稱為頂級域。網(wǎng)上注冊的域名基本都是二級域名(2個層級),比如http://baidu.com、http://taobao.com等等二級域名,它們基本上是歸企業(yè)和運維人員管理,接下來是三級或者四級域名,這里不多贅述。

域名解析的過程如圖:
在這里插入圖片描述
解析成功后,系統(tǒng)或者瀏覽器一般都會進行DNS緩存。

可以使用dig工具分析DNS過程,Centos下安裝dig工具:

yum install bind-utils

使用方法很簡單,可以參考dig使用,例如

dig www.baidu.com

ICMP協(xié)議

ICMP協(xié)議(Internet Control Message Protocol,互聯(lián)網(wǎng)控制消息協(xié)議)是一個網(wǎng)絡層協(xié)議,主要用于在IP網(wǎng)絡中發(fā)送錯誤報告以及其他需要注意的信息。
在這里插入圖片描述

ICMP 主要功能包括

  • 確認 IP 包是否成功到達目標地址
  • 通知在發(fā)送過程中 IP 包被丟棄的原因

ICMP 也是基于 IP 協(xié)議工作的它只能搭配 IPv4 使用,如果是 IPv6 需要使用 ICMPv6

在源主機向目標主機發(fā)送報文時,當目標主機或中途某個節(jié)點出現(xiàn)某些錯誤,如目標主機斷電關機,而源主機是無法獲取這些情況的,如果源主機希望了解這些情況,就需要距離故障節(jié)點最近的節(jié)點向源主機返回一個匯報情況的報文
在這里插入圖片描述
ICMP協(xié)議格式如下:
在這里插入圖片描述
類型的值如下:
在這里插入圖片描述
可以大致將報文分為2類,一類是通知出錯原因,一類是用于診斷查詢。

代理服務器

代理服務器又分為正向代理和反向代理,是一種應用比較廣的技術,例如

  • 翻墻:廣域網(wǎng)中的代理
  • 負載均衡:局域網(wǎng)中的代理

正向代理用于請求的轉發(fā)(例如借助代理繞過反爬蟲),反向代理往往作為一個緩存。

正向代理

正向代理(Forward Proxy)是一種常見的網(wǎng)絡代理方式,它位于客戶端和目標服務器之間,代表客戶端向目標服務器發(fā)送請求。正向代理服務器接收客戶端的請求,然后將請求轉發(fā)給目標服務器,最后將目標服務器的響應返回給客戶端。通過這種方式,正向代理可以實現(xiàn)多種功能,如提高訪問速度、隱藏客戶端身份、實施訪問控制等。
在這里插入圖片描述

  1. 工作原理
    客戶端將請求發(fā)送給正向代理服務器,正向代理服務器接收請求,并根據(jù)配置進行處理,如緩存查找、內(nèi)容過濾等,然后將處理后的請求轉發(fā)給目標服務器,目標服務器對接收到的請求做處理,并將響應返回給正向代理服務器,最后正向代理服務器將響應返回給客戶端。

  2. 功能特點

  • 緩存功能:正向代理服務器可以緩存經(jīng)常訪問的資源,當客戶端再次請求這些資源時,可以直接從緩存中獲取,提高訪問速度。
  • 內(nèi)容過濾:正向代理可以根據(jù)預設的規(guī)則對請求或響應進行過濾,如屏蔽廣告、阻止惡意網(wǎng)站等。
  • 訪問控制:通過正向代理,可以實現(xiàn)對特定網(wǎng)站的訪問控制,如限制員工在工作時間訪問娛樂網(wǎng)站。
  • 隱藏客戶端身份:正向代理可以隱藏客戶端的真實 IP 地址,保護客戶端的隱私。
  • 負載均衡:在多個目標服務器之間分配客戶端請求,提高系統(tǒng)的可擴展性和可靠性。
  1. 應用場景
  • 企業(yè)網(wǎng)絡管理:企業(yè)可以通過正向代理實現(xiàn)對員工網(wǎng)絡訪問的管理和控制,確保員工在工作時間內(nèi)專注于工作,避免訪問不良網(wǎng)站或泄露公司機密。
  • 公共網(wǎng)絡環(huán)境:在公共場所如圖書館、學校等提供的網(wǎng)絡環(huán)境中,通過正向代理可以實現(xiàn)對網(wǎng)絡資源的合理分配和管理,確保網(wǎng)絡使用的公平性和安全性。
  • 內(nèi)容過濾與保護:家長可以通過設置正向代理來過濾不良內(nèi)容,保護孩子免受網(wǎng)絡上的不良信息影響。
  • 提高訪問速度:對于經(jīng)常訪問的網(wǎng)站或資源,正向代理可以通過緩存機制提高訪問速度,減少網(wǎng)絡延遲。
  • 跨境電商與海外訪問:對于跨境電商或需要訪問海外資源的企業(yè)和個人,正向代理可以幫助他們突破網(wǎng)絡限制,順暢地訪問海外網(wǎng)站和資源。
反向代理

反向代理服務器是一種網(wǎng)絡架構模式,其作為 Web 服務器的前置服務器,接收來自客戶端的請求,并將這些請求轉發(fā)給后端服務器,然后將后端服務器的響應返回給客戶端。這種架構模式可以提升網(wǎng)站性能、安全性和可維護性等
在這里插入圖片描述

  1. 基本原理

反向代理服務器位于客戶端和 Web 服務器之間,當客戶端發(fā)起請求時,它首先會到達反向代理服務器。反向代理服務器會根據(jù)配置的規(guī)則將請求轉發(fā)給后端的 Web服務器,并將 Web 服務器的響應返回給客戶端。在這個過程中,客戶端并不知道實際與哪個 Web 服務器進行了交互,它只知道與反向代理服務器進行了通信。CDN(Content Delivery Network,內(nèi)容分發(fā)網(wǎng)絡)就是采用了反向代理的原理。

  1. 應用場景
  • 負載均衡:反向代理服務器可以根據(jù)配置的負載均衡策略,將客戶端的請求分發(fā)到多個后端服務器上,以實現(xiàn)負載均衡。這有助于提升網(wǎng)站的整體性能和響應速度,特別是在高并發(fā)場景下。
  • 安全保護:反向代理服務器可以隱藏后端 Web 服務器的真實 IP 地址,降低其被直接攻擊的風險。同時,它還可以配置防火墻、訪問控制列表(ACL)等安全策略,對客戶端的請求進行過濾和限制,以保護后端服務器的安全。
  • 緩存加速:反向代理服務器可以緩存后端 Web 服務器的響應內(nèi)容,對于重復的請求,它可以直接從緩存中返回響應,而無需再次向后端服務器發(fā)起請求。這可以大大減少后端服務器的負載,提升網(wǎng)站的響應速度。
  • 內(nèi)容過濾和重寫:反向代理服務器可以根據(jù)配置的規(guī)則對客戶端的請求進行過濾和重寫,例如添加或刪除請求頭、修改請求路徑等。這有助于實現(xiàn)一些特定的業(yè)務需求,如 URL 重寫、用戶認證等。
  • 動靜分離:在大型網(wǎng)站中,通常需要將靜態(tài)資源和動態(tài)資源分開處理。通過將靜態(tài)資源部署在反向代理服務器上,可以直接從反向代理服務器返回靜態(tài)資源的響應,而無需再次向后端服務器發(fā)起請求。這可以大大提升靜態(tài)資源的訪問速度。
NAT 和代理服務器

路由器往往都具備 NAT 設備的功能,通過 NAT 設備進行中轉,完成子網(wǎng)設備和其他子網(wǎng)設備的通信過程。代理服務器看起來和 NAT 設備有一點像??蛻舳讼虼矸掌靼l(fā)送請求,代理服務器將請求轉發(fā)給真正要請求的服務器,服務器返回結果后,代理服務器又把結果回傳給客戶端,那么 NAT 和代理服務器的區(qū)別有哪些呢?

  • 從應用上講, NAT 設備是網(wǎng)絡基礎設備之一,解決的是 IP 不足的問題,代理服務器則是更貼近具體應用,比如通過代理服務器進行翻墻,另外像迅游這樣的加速器,也是使用代理服務器
  • 從底層實現(xiàn)上講, NAT 是工作在網(wǎng)絡層,直接對 IP 地址進行替換,代理服務器往往工作在應用層
  • 從使用范圍上講,NAT 一般在局域網(wǎng)的出口部署,代理服務器可以在局域網(wǎng)做,也可以在廣域網(wǎng)做,也可以跨網(wǎng)
  • 從部署位置上看,NAT 一般集成在防火墻,路由器等硬件設備上,代理服務器則是一個軟件程序,需要部署在服務器上.

內(nèi)網(wǎng)穿透

如果我在自己家里的電腦上部署了一個小服務S,如果我現(xiàn)在在學校,我該怎么取得家里電腦的服務呢?直接拿家里電腦的私有IP進行訪問肯定是不行的,此時我們需要在公網(wǎng)上部署一個服務M,先讓家里的小服務S向服務M發(fā)起一個TCP請求,這樣S和M之間就有了一條通信鏈路,此后其他客戶端向M發(fā)起的請求都可以轉發(fā)給S,S處理請求后將請求結果返回M,再由M轉交給客戶端。像M這種服務,現(xiàn)在已經(jīng)有很多現(xiàn)成的了,如frps等。
在這里插入圖片描述
其實利用一個中間服務器的轉發(fā)功能,將請求從一個網(wǎng)絡轉發(fā)到另一個網(wǎng)絡。

內(nèi)網(wǎng)打洞

現(xiàn)在有2個客戶端主機A和B,A和B都向公網(wǎng)中的服務器M發(fā)起tcp連接,這樣A和B到各自出口路由器的鏈路上都建立起了NAT轉換表,此時服務器M做了這樣一件事:將A的出口路由器的IP和端口號告訴B,同時也將B的出口路由器的IP和端口號告訴A,此后,A和B都直接向得到的IP+port發(fā)送報文,A和B就實現(xiàn)了互相通信,而且通信不必再經(jīng)過服務器M,這就極大的減輕了服務器M的壓力,QQ通信就是利用了這個原理。

在這里插入圖片描述

高級IO

IO的本質(zhì)就是等待條件就緒+數(shù)據(jù)拷貝,如果單位時間內(nèi)等的比重越低,那么IO的效率就越高。

五種IO模型

  1. 阻塞IO

阻塞 IO就是在內(nèi)核將數(shù)據(jù)準備好之前,系統(tǒng)調(diào)用會一直等待,所有的套接字,默認都是阻塞方式,陷入阻塞IO的線程不占用CPU資源,此期間線程無法往下推進執(zhí)行后續(xù)代碼。
在這里插入圖片描述

  1. 非阻塞IO

非阻塞 IO就是如果內(nèi)核還未將數(shù)據(jù)準備好,系統(tǒng)調(diào)用會直接返回并且返回EWOULDBLOCK 錯誤碼,此時線程就可以往后繼續(xù)執(zhí)行其他代碼。但非阻塞 IO 往往需要程序員循環(huán)的方式反復嘗試讀寫文件描述符,這個過程稱為輪詢,這對 CPU 來說是較大的浪費,一般只有特定場景下才使用。
在這里插入圖片描述

文件描述符默認都是阻塞IO,我們通過系統(tǒng)調(diào)用 fcntl 將某個文件描述符設置為非阻塞IO。

#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );

fcntl的返回值與執(zhí)行的命令有關。如果操作成功,則返回特定于命令的值(如新的文件描述符、文件描述符標記、文件狀態(tài)標記等)。如果操作失敗,則返回-1,并設置errno以指示錯誤類型,參數(shù)說明:
fd:需要設置的文件描述符
cmd:操作指令,取值如下

  • F_DUPFD:復制一個現(xiàn)有的描述符(cmd=)
  • F_GETFD 或 F_SETFD:獲得/設置文件描述符標記、
  • F_GETFL 或 F_SETFL:獲得/設置文件狀態(tài)標記
  • F_GETOWN 或 F_SETOWN:獲得/設置異步 I/O 所有權
  • F_GETLK,F_SETLK 或 F_SETLKW:獲得/設置記錄鎖

如下,我們就實現(xiàn)了將一個文件描述符設置為非阻塞:

void setNonBlock(int fd) {  int fl = fcntl(fd, F_GETFL); // 獲取文件描述符的當前狀態(tài)標記  if (fl < 0) {  perror("fcntl");  return;  }  fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 設置文件描述符為非阻塞模式  
}  
  1. 信號驅動IO

信號驅動 IO就是內(nèi)核將數(shù)據(jù)準備好的時候,使用 SIGIO 信號通知應用程序進行 IO操作。此期間線程可以往后繼續(xù)執(zhí)行其他代碼,等到 SIGIO 信號來臨時再進行IO和數(shù)據(jù)處理。
在這里插入圖片描述

  1. IO多路轉接

IO 多路轉接也稱為IO多路復用,能夠同時等待多個文件描述符的就緒狀態(tài),如果其中任何一個文件描述符就緒了,系統(tǒng)調(diào)用就直接返回。
在這里插入圖片描述

  1. 異步IO

異步IO就是由內(nèi)核在數(shù)據(jù)拷貝完成時,通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數(shù)據(jù),其將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間仍需要一段時間,期間線程時處于阻塞狀態(tài)的)。此期間線程可以往后繼續(xù)執(zhí)行其他代碼,等到內(nèi)核通知線程時直接進行數(shù)據(jù)處理。
在這里插入圖片描述

在前面的IO模型中,前四種屬于同步IO,其中IO多路轉接的方式效率最高,因為它一次等待多個文件描述符,平均下來單位時間內(nèi)等的比重較低。

紀錄鎖、系統(tǒng) V 流機制、I/O 多路轉接、readv 和writev 函數(shù)以及存儲映射 IO(mmap)都統(tǒng)稱為高級IO,我們主要討論I/O 多路轉接。

IO多路轉接

select

系統(tǒng)提供 select 函數(shù)來實現(xiàn)多路復用輸入/輸出模型,它可以用來讓我們的程序監(jiān)視多個文件描述符的狀態(tài)變化的,當程序執(zhí)行到select時,程序會停在這里等待,直到被監(jiān)視的文件描述符有一個或多個發(fā)生了狀態(tài)改變。

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set*exceptfds, struct timeval *timeout);

其返回值如下:

  • 正值:當select函數(shù)返回正值時,表示有相應數(shù)量的文件描述符已經(jīng)就緒(如果此次數(shù)據(jù)沒有讀完,下次select會立即就緒),可以進行I/O操作。這個正值就是就緒文件描述符的總數(shù)。調(diào)用者可以通過檢查傳入的文件描述符集合(readfds、writefds、exceptfds)來確定哪些文件描述符已經(jīng)就緒。
  • 零:如果select函數(shù)返回0,則表示在指定的超時時間內(nèi),沒有任何文件描述符就緒。這通常意味著所有的I/O操作都在等待中,或者已經(jīng)設置的超時時間已經(jīng)到期。
  • 負值:當select函數(shù)返回-1時,表示調(diào)用過程中發(fā)生了錯誤。此時,通常會設置全局變量errno來指示具體的錯誤類型。例如,如果select函數(shù)被信號中斷,errno可能會被設置為EINTR。

參數(shù)說明:

nfds:要監(jiān)視的文件描述符集合中的最大值加1。文件描述符是從0開始計數(shù)的整數(shù),因此這個參數(shù)實際上是文件描述符集合中最大文件描述符的值加1。

readfds:用于指定要監(jiān)視其讀狀態(tài)變化的文件描述符集合。如果對某個文件描述符的讀狀態(tài)感興趣,則應該將其添加到這個集合中。
writefds:用于指定要監(jiān)視其寫狀態(tài)變化的文件描述符集合。如果對某個文件描述符的寫狀態(tài)感興趣,則應該將其添加到這個集合中。
exceptfds:用于指定要監(jiān)視其異常狀態(tài)變化的文件描述符集合。如果某個文件描述符的異常狀態(tài)感興趣,則應該將其添加到這個集合中。
timeout:指向timeval結構的指針,用于指定select函數(shù)的超時時間。timeval結構包含兩個成員:tv_sec(秒)和tv_usec(微秒),取值如下:

  • NULL值:表示select函數(shù)將一直阻塞,直到有文件描述符就緒或發(fā)生錯誤。
  • 0值:如果將tv_sec和tv_usec都設置為0,則select函數(shù)將立即返回,而不進行任何阻塞。
  • 正值:如果tv_sec和/或tv_usec設置為正值,則select函數(shù)將在指定的超時時間內(nèi)阻塞,等待文件描述符就緒。如果在超時時間內(nèi)有文件描述符就緒,則select函數(shù)將返回;否則,在超時后返回。

其中fd_set結構體的定義如下:
在這里插入圖片描述
其實這個結構就是一個整數(shù)數(shù)組,更嚴格的說,是一個 “位圖”,使用位圖中對應的位來表示要監(jiān)視的文件描述符,系統(tǒng)提供了一組操作 fd_set 的接口,以方便用戶操作這個位圖:

void FD_CLR(int fd, fd_set *set); // 用來清除set中fd的位
int FD_ISSET(int fd, fd_set *set); // 用來檢測fd是否在set中
void FD_SET(int fd, fd_set *set); // 用來將fd設置到set中
void FD_ZERO(fd_set *set); // 用來清除set的全部位

select的特點:

  • 可監(jiān)控的文件描述符個數(shù)取決于 sizeof(fd_set)的值,一般
    sizeof(fd_set)=512,每 bit 表示一個文件描述符,則支持的最大文件描述符是 512*8=4096,可以通過某些手段調(diào)整fd_set 的大小,這可能涉及到重新編譯內(nèi)核
  • select返回后,fd_set結構體中對應事件依舊沒有就緒的位會被置0,因此將 fd 加入 select 監(jiān)控集的同時,還要再使用一個數(shù)據(jù)結構 array 保存放到 select監(jiān)控集中的 fd,一是用于在 select 返回后,array 作為源數(shù)據(jù)和 fd_set 進行 FD_ISSET 判斷,二是 select 返回后會把以前加入的但并無事件發(fā)生的 fd 清空,則每次開始select 前都要重新從 array 取得 fd 逐一加入fd_set結構體(應先用FD_ZERO將該結構體清空),掃描 array 的同時取得 fd 最大值 maxfd,用于 select 的第一個參數(shù)。

select的缺點:

  • 每次調(diào)用 select都需要手動設置 fd 集合(本質(zhì)原因就是對應的fd_set結構體既作為輸入?yún)?shù)又作為輸出參數(shù),輸入和輸出重合),從接口使用角度來說也非常不便
  • 每次調(diào)用 select,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在 fd 很多時會很大(因為文件描述符集合是保存在用戶態(tài)的內(nèi)存空間中的。但是,為了檢查這些文件描述符是否有I/O事件發(fā)生,內(nèi)核需要在其自己的內(nèi)存空間中訪問這些文件描述符),同時每次調(diào)用 select 都需要在內(nèi)核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大(如果遍歷完所有被監(jiān)視的文件描述符后沒有發(fā)現(xiàn)任何I/O事件發(fā)生,并且沒有指定超時(或者超時時間未到),那么select函數(shù)通常會進入阻塞狀態(tài),進程會掛起(即進入睡眠狀態(tài)),直到某個被監(jiān)視的文件描述符上有I/O事件(如可讀、可寫或異常事件)發(fā)生,或者超時時間到達)
  • select 支持的文件描述符數(shù)量太小

最后,我們看看網(wǎng)絡中socket文件描述符的讀寫就緒和異常就緒情況:

  1. socket讀就緒:
  • socket 內(nèi)核中,接收緩沖區(qū)中的字節(jié)數(shù)大于等于低水位標記
    SO_RCVLOWAT,此時可以無阻塞的讀該文件描述符,并且返回值大于 0
  • socket TCP 通信中,對端關閉連接,此時對該 socket 讀,則返回 0
  • 監(jiān)聽的 socket 上有新的連接請求
  • socket 上有未處理的錯誤
  1. socket寫就緒:
  • socket 內(nèi)核中,發(fā)送緩沖區(qū)中的可用字節(jié)數(shù)(發(fā)送緩沖區(qū)的空閑位置大小)大于等于低水位標記 SO_SNDLOWAT,此時可以無阻塞的寫,并且返回值大于 0
  • socket 的寫操作被關閉(close 或者 shutdown),對一個寫操作被關閉的 socket進行寫操作,會觸發(fā) SIGPIPE 信號
  • socket 使用非阻塞 connect 連接成功或失敗之后
  • socket 上有未讀取的錯誤
  1. 異常就緒:
  • socket 上收到帶外數(shù)據(jù)

poll

同select一樣,poll是系統(tǒng)提供的用來實現(xiàn)多路復用輸入/輸出模型的函數(shù),可以讓我們的程序監(jiān)視多個文件描述符的狀態(tài)變化。當程序執(zhí)行到poll時,程序會停在這里等待,直到被監(jiān)視的文件描述符有一個或多個發(fā)生了狀態(tài)改變。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd 結構
struct pollfd {int fd; /* file descriptor  文件描述符  */short events; /* requested events 要監(jiān)控的事件類型  */short revents; /* returned events 實際發(fā)生的事件類型 */
};

該函數(shù)調(diào)用成功時,poll 返回發(fā)生事件的文件描述符數(shù)量。如果在調(diào)用時所有文件描述符都不滿足條件,并且 timeout 毫秒已經(jīng)過去,則返回 0。如果調(diào)用失敗,則返回 -1,并設置 errno 以指示錯誤類型。參數(shù)說明:
fds :是一個 poll 函數(shù)監(jiān)聽的結構列表,每一個元素中,包含了三部分內(nèi)容:文件描述符, 監(jiān)聽的事件集合, 返回的事件集合

nfds :表示 fds 數(shù)組的長度

timeout:表示 poll 函數(shù)的超時時間,單位是毫秒(ms)

events 和 revents 的取值:
在這里插入圖片描述
我們一般只關心POLLIN和POLLOUT,即讀事件和寫事件。

poll的優(yōu)點:

  • 不同于 select 使用三個位圖來表示三個 fdset 的方式,poll 使用一個 pollfd 的指針實現(xiàn),pollfd 結構包含了要監(jiān)視的事件和發(fā)生的事件,不再使用 select“參數(shù)-值”傳遞的方式,接口使用比 select 更方便
  • poll 并沒有最大數(shù)量限制 (但是數(shù)量過大后性能也是會下降)

poll的缺點:

  • 和 select 函數(shù)一樣,poll 返回后,需要輪詢 pollfd 來獲取就緒的描述符,當監(jiān)聽的文件描述符數(shù)目增多時,十分影響性能
  • 每次調(diào)用 poll 都需要把大量的 pollfd 結構從用戶態(tài)拷貝到內(nèi)核中,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長,其效率也會下降

總之,poll不需要像select一樣在每次調(diào)用時都重新初始化文件描述符集,也沒有文件描述符數(shù)量限制,解決了select的大部分缺點問題,但隨著文件描述符數(shù)量的增加所帶來的監(jiān)視性能下降問題依舊沒有解決。

epoll

epoll是為處理大批量句柄而作了改進的 poll,它是在 2.5.44 內(nèi)核中被引進的,幾乎具備了之前所說的一切優(yōu)點,被公認為 Linux2.6 下性能最好的多路 I/O 就緒通知方法。

1. 創(chuàng)建一個epoll句柄:

#include <sys/epoll.h>  
int epoll_create(int size);

成功時,epoll_create 返回一個非負的文件描述符,該描述符用于后續(xù)對 epoll 實例的操作(如添加、刪除監(jiān)控的文件描述符,以及等待事件)。失敗時,返回 -1,并設置 errno 以指示錯誤原因。參數(shù)說明:
size :在較新的 Linux 內(nèi)核版本中該參數(shù)已經(jīng)被忽略,它曾經(jīng)用于指示期望監(jiān)控的文件描述符數(shù)量。即使現(xiàn)在被忽略,許多程序仍然傳遞一個值(如 1024)作為 size,以保持向后兼容性。

需要注意,在使用完之后,必須調(diào)用 close()關閉返回的文件描述符。

2. epoll事件注冊:

#include <sys/epoll.h>  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);

成功時epoll_ctl 返回 0,失敗時返回 -1,并設置 errno 以指示錯誤原因,參數(shù)說明:
epfd:創(chuàng)建epoll句柄時返回的文件描述符

op:要執(zhí)行的操作,可以是以下三個值之一:

  • EPOLL_CTL_ADD:向 epoll 實例中添加一個新的文件描述符進行監(jiān)控。
  • EPOLL_CTL_DEL:從 epoll 實例中刪除一個文件描述符,停止對其的監(jiān)控。
  • EPOLL_CTL_MOD:修改一個已經(jīng)存在于 epoll 實例中的文件描述符的監(jiān)控事件。

fd:要添加、刪除或修改的文件描述符。

event:指向一個 epoll_event 結構體的指針,該結構指定了要監(jiān)控的事件類型及與文件描述符相關聯(lián)的數(shù)據(jù)(對于 EPOLL_CTL_ADD 和 EPOLL_CTL_MOD 操作)。對于 EPOLL_CTL_DEL 操作,此參數(shù)可以是 NULL。

其中 struct epoll_event 結構體定義如下:

struct epoll_event {  __uint32_t events;      // 要監(jiān)控的事件類型(如讀、寫、異常等)  epoll_data_t data;      // 與文件描述符相關聯(lián)的數(shù)據(jù),可以是 `void *`、`int` 或 `uint32_t` 類型  
};  typedef union epoll_data {  void    *ptr;  int      fd;  uint32_t u32;  uint64_t u64;  
} epoll_data_t;

events 即我們想要監(jiān)視的文件描述符的事件的集合,它的取值有:

  • EPOLLIN : 表示對應的文件描述符可以讀 (包括對端 SOCKET 正常關閉)
  • EPOLLOUT : 表示對應的文件描述符可以寫
  • EPOLLPRI : 表示對應的文件描述符有緊急的數(shù)據(jù)可讀 (這里應該表示有帶外數(shù)據(jù)到來)
  • EPOLLERR : 表示對應的文件描述符發(fā)生錯誤
  • EPOLLHUP : 表示對應的文件描述符被掛斷
  • EPOLLET : 將 EPOLL 設為邊緣觸發(fā)(Edge Triggered)模式, 這是相對于水平觸發(fā)(Level Triggered)來說的
  • EPOLLONESHOT:只監(jiān)聽一次事件, 當監(jiān)聽完這次事件之后, 如果還需要繼續(xù)監(jiān)聽這個 socket 的話, 需要再次把這個 socket 加入到 EPOLL 隊列里

data 字段是一個epoll_data_t類型的聯(lián)合體,允許我們存儲與文件描述符相關聯(lián)的任意類型的數(shù)據(jù),這個數(shù)據(jù)在事件發(fā)生時會被返回,以便我們識別是哪個文件描述符觸發(fā)了事件,并據(jù)此執(zhí)行相應的操作,也可以讓開發(fā)者實現(xiàn)復雜的事件處理邏輯,并優(yōu)化程序的性能和響應速度。它提供了四種方式來存儲與文件描述符相關聯(lián)的數(shù)據(jù):

  • ptr:一個通用指針,可以指向任何類型的數(shù)據(jù)。
  • fd:一個文件描述符,這在某些情況下很有用,比如當你想要將事件與另一個文件描述符相關聯(lián)時。
  • u32:提供了32位的無符號整數(shù)存儲,可以用于存儲任何可以表示為整數(shù)的數(shù)據(jù)。
  • u64:提供了64位的無符號整數(shù)存儲,可以用于存儲任何可以表示為整數(shù)的數(shù)據(jù)。

3. epoll事件等待:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);

如果成功epoll_wait 返回就緒事件的數(shù)量,這個值可能小于或等于 maxevents;如果 timeout 到期且沒有事件發(fā)生則返回 0;失敗時返回 -1,并設置 errno 以指示錯誤原因。參數(shù)說明:

epfd:創(chuàng)建epoll句柄時返回的文件描述符

events:指向一個 epoll_event 結構數(shù)組的指針,該數(shù)組用于存儲就緒事件的信息,調(diào)用者需要分配足夠的空間來存儲最多 maxevents 個事件

maxevents:events 數(shù)組可以存儲的最大事件數(shù)。這個值也限制了 epoll_wait 可以一次性返回的最大事件數(shù)

timeout:等待事件的超時時間(以毫秒為單位),如果 timeout 為 -1,epoll_wait 將無限期地阻塞,直到有事件發(fā)生,如果 timeout 為 0,epoll_wait 將立即返回,即使沒有任何事件發(fā)生(這種情況下,返回值將是 0),如果 timeout 大于 0,epoll_wait 將阻塞最多 timeout 毫秒,然后返回。

下面簡單介紹一下epoll的工作原理:
在這里插入圖片描述
每一個 epoll 句柄都有一個獨立的 eventpoll 結構體,當我們使用epoll_create創(chuàng)建一個epoll句柄時,Linux內(nèi)核就會創(chuàng)建一個evenpoll對象,該結構體比較重要成員就是rdllist(一個就緒隊列鏈表)和rbr(一顆紅黑樹)。當我們使用epoll_ctl將一個新的文件描述符添加到epoll實例中時,就是在該紅黑樹中新增一個節(jié)點(epitem對象),因此使用epoll_ctl對epoll實例的操作就是對該紅黑樹進行增刪查改,這樣重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是 lgn,其中 n 為樹的高度)。所有添加到 epoll 中的事件都會與設備(網(wǎng)卡)驅動程序建立回調(diào)關系,當響應的事件發(fā)生時會調(diào)用這個回調(diào)方法(內(nèi)核中這個回調(diào)方法叫 ep_poll_callback),它會將發(fā)生的事件嚴格按照發(fā)生的先后順序添加到 rdllist 雙鏈表中。

struct epitem{struct rb_node rbn;//紅黑樹節(jié)點struct list_head rdllink;//雙向鏈表節(jié)點struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所屬的 eventpoll 對象struct epoll_event event; //期待發(fā)生的事件類型
}

我們注意到epitem中有一個成員struct list_head rdllink,如果回調(diào)函數(shù)想將事件就緒節(jié)點加入到就緒鏈表,只需要將利用rdllink指針就可以將當前節(jié)點鏈入到就緒隊列中,從而省去了拷貝的開銷(如果是監(jiān)視多個事件但只有某些事件就緒了,內(nèi)核并會創(chuàng)建一個簡化的結構體或數(shù)據(jù)結構(通常包含事件類型和對應的文件描述符),來表明當前就緒的這些事件,并將其鏈入到就緒隊列中)。

當調(diào)用 epoll_wait 檢查是否有事件發(fā)生時,只需要檢查 eventpoll 對象中的 rdlist 雙鏈表中是否有 epitem 元素即可,如果 rdlist 不為空,則把發(fā)生的事件復制到用戶態(tài),同時將事件數(shù)量返回給用戶(由于就緒鏈表中的節(jié)點數(shù)量通常相對較少,這個操作的時間復雜度近似O(1))。

epoll的優(yōu)點:
3. 接口使用方便,雖然拆分成了三個函數(shù),但是反而使用起來更方便高效,不需要每次循環(huán)都設置關注的文件描述符,也做到了輸入輸出參數(shù)分離開
4. 數(shù)據(jù)拷貝輕量:只在合適的時候調(diào)用 EPOLL_CTL_ADD 將文件描述符結構拷貝到內(nèi)核中(例如當添加新的文件描述符到監(jiān)視列表時),這個操作并不頻繁(而 select/poll 都是每次循環(huán)都要進行拷貝)
5. 事件回調(diào)機制:避免使用遍歷,而是使用回調(diào)函數(shù)的方式將就緒的文件描述符結構加入到就緒隊列中,epoll_wait 返回直接訪問就緒隊列就知道哪些文件描述符就緒,這個操作時間復雜度 O(1),即使文件描述符數(shù)目很多,效率也不會受到影響
6. 沒有數(shù)量限制:文件描述符數(shù)目無上限

epoll的工作方式:
epoll 有 2 種工作方式:水平觸發(fā)(LT)和邊緣觸發(fā)(ET)

  1. 水平觸發(fā) Level Triggered 工作模式:
    當 epoll 檢測到某個文件描述符上事件就緒的時候,可以不立刻進行處理或者只處理一部分,例如,讀緩沖區(qū)有2K數(shù)據(jù),可以只讀 1K 數(shù)據(jù),緩沖區(qū)中還剩 1K 數(shù)據(jù),在第二次調(diào)用epoll_wait 時,epoll_wait 會立刻返回并通知該文件描述符讀事件就緒,直到緩沖區(qū)上所有的數(shù)據(jù)都被處理完,epoll_wait 才不會立刻返回,epoll 默認狀態(tài)下就是 LT 工作模式。

  2. 邊緣觸發(fā) Edge Triggered 工作模式:
    當 epoll 檢測到某個文件描述符上事件就緒時,必須立刻處理,如上面的例子中,如果只讀了 1K 的數(shù)據(jù),緩沖區(qū)還剩 1K 的數(shù)據(jù),在第二次調(diào)用epoll_wait 的時候,epoll_wait 不會立刻返回了,此后如果沒有通知用戶不敢貿(mào)然讀取緩沖區(qū)中剩下的數(shù)據(jù)(因為用戶只知道自己讀了1K的數(shù)據(jù),但不確定自己讀的這1K究竟是緩沖區(qū)數(shù)據(jù)的一部分還是全部),除非緩沖區(qū)中數(shù)據(jù)變多,epoll_wait 才會返回。也就是說ET 模式下,文件描述符上的事件就緒后只有一次處理機會(注意不是指一次read的機會)。 Nginx (一個高性能的HTTP和反向代理web服務器)默認采用ET 模式使用 epoll

使用 ET 能夠減少 epoll_wait 觸發(fā)的次數(shù),因此ET 的性能比 LT 性能更高(這也意味著ET 的代碼復雜程度更高),其實在 LT 情況下如果能做到每次就緒的文件描述符都立刻處理,不讓這個就緒被重復提示的話,它們的性能是一樣的。

最后需要注意的是,使用 ET 模式的 epoll,需要將文件描述設置為非阻塞,這個不是接口上的要求,而是 “工程實踐” 上的要求(LT工作方式?jīng)]有這個要求,可以對文件描述符的阻塞讀寫和非阻塞讀寫),因為ET模式下,epoll只通知我們一次緩沖區(qū)中有沒有數(shù)據(jù)可以讀寫,在使用read讀數(shù)據(jù)時,我們不能保證一次就把緩沖區(qū)的數(shù)據(jù)剛好讀完,需要循環(huán)多次讀取,如果該文件描述符是阻塞讀取的話,最后一次讀取必然會阻塞,如果我們將該文件描述符設為非阻塞,最后一次讀取如果沒有出錯返回值必定是0(只要檢查錯誤碼errno == EWOULDBLOCK || errno == EAGAIN就可以知道是否是出錯返回。如果在一個設置為非阻塞模式的文件描述符讀寫失敗時就會將錯誤碼設置為 EWOULDBLOCK;如果在非阻塞模式下嘗試讀取一個當前沒有數(shù)據(jù)的文件或套接字,或者向一個當前無法寫入數(shù)據(jù)的文件或套接字寫入數(shù)據(jù),就會將錯誤碼設置為 EAGAIN。而且每一個線程都有自己的錯誤碼error),此時就不會被阻塞,接下來就可以繼續(xù)調(diào)用epoll_wait等待下次緩沖區(qū)數(shù)據(jù)到來。如果是LT模式,只有epoll_wait返回表示事件就緒我們才進行讀寫,無論該文件描述符是阻塞還是非阻塞,我們都不會被阻塞。

在使用epoll時,對于讀,我們要先設置對某個文件描述符的監(jiān)視,讀條件就緒了再讀;對于寫,我們一般是直接向文件描述符寫,如果寫完了,那么此次寫就結束了,如果寫出錯返回(在非阻塞模式下寫緩沖區(qū)滿會返回EAGAIN或EWOULDBLOCK錯誤碼,當然也有極小的概率此時數(shù)據(jù)剛剛好寫完了),意味著寫條件不滿足,此時再用epoll設置對該文件描述符寫事件的監(jiān)視,寫完后就解除對該寫事件的監(jiān)視。這樣做是因為一般緩沖區(qū)大部分時間都是空的,直接讀很可能會讀到空,而直接寫很可能立即就成功了。

epoll 的使用場景:
epoll 的高性能是有一定的特定場景的,如果場景選擇的不適宜,epoll 的性能可能適得其反。對于多連接且多連接中只有一部分連接比較活躍時,比較適合使用 epoll,例如典型的一個需要處理上萬個客戶端的服務器、各種互聯(lián)網(wǎng) APP 的入口服務器就很適合 epoll。如果只是系統(tǒng)內(nèi)部,服務器和服務器之間進行通信,只有少數(shù)的幾個連接,這種情況下用epoll 就并不合適(epoll在內(nèi)核中需要維護一些數(shù)據(jù)和結構,在連接數(shù)較少時,這些開銷是不值得的,此時使用epoll并不能帶來性能上的顯著提升),因此要根據(jù)具體需求和場景特點來決定使用哪種 IO 模型。

http://m.risenshineclean.com/news/63694.html

相關文章:

  • 網(wǎng)站權重怎么提升可以搜索國外網(wǎng)站的搜索引擎
  • 商丘做網(wǎng)站公司新站seo快速收錄網(wǎng)頁內(nèi)容頁的方法如何制作公司網(wǎng)頁
  • 廣州網(wǎng)站建設公司興田德潤怎么樣搜收錄網(wǎng)
  • 起名網(wǎng)站怎么做免費做網(wǎng)站怎么做網(wǎng)站嗎
  • 網(wǎng)站上那些兼職網(wǎng)頁怎么做的搜索引擎優(yōu)化的辦法有哪些
  • 用asp做的網(wǎng)站有多少沈陽cms建站模板
  • 順德企業(yè)手機網(wǎng)站建設網(wǎng)絡營銷的方法有哪些?
  • 佛山網(wǎng)站優(yōu)化美姿姿seo百度競價點擊軟件
  • 企業(yè)網(wǎng)站 實名認證視頻號視頻怎么看下載鏈接
  • 做一個網(wǎng)站后期維護需要多少錢seo包年優(yōu)化平臺
  • 深圳本地做網(wǎng)站網(wǎng)絡營銷的策略包括
  • 做國際物流在哪些網(wǎng)站找客戶steam交易鏈接怎么看
  • vs做網(wǎng)站怎樣添加圖片網(wǎng)絡黃頁推廣大全
  • 綠色風格的網(wǎng)站seo發(fā)帖軟件
  • 網(wǎng)頁設計與網(wǎng)站建設教學視頻百度收錄站長工具
  • 類似餓了么的網(wǎng)站怎么做淘寶排名查詢
  • 網(wǎng)站開發(fā)課程培訓seo工具查詢
  • 做動漫網(wǎng)站需要服務器么友鏈購買網(wǎng)
  • 合肥企業(yè)網(wǎng)站建設工作室網(wǎng)站快速收錄
  • 免費網(wǎng)站免費網(wǎng)站平臺百度推廣登陸入口
  • php做購物網(wǎng)站詳情頁的代碼網(wǎng)絡推廣方案例子
  • 大連做網(wǎng)站紹興廠商刷粉網(wǎng)站推廣便宜
  • 企業(yè)網(wǎng)站托管注意事項蘇州seo門戶網(wǎng)
  • wordpress加載不出圖黑帽seo排名
  • 微信網(wǎng)站如何做如何外貿(mào)推廣
  • 為什么做免費視頻網(wǎng)站網(wǎng)站網(wǎng)絡優(yōu)化外包
  • 做網(wǎng)站推銷好做嗎如何讓百度收錄
  • 順德網(wǎng)站制作公司哪家好品牌推廣宣傳詞
  • 騰訊企業(yè)郵箱網(wǎng)頁版登錄入口滄州網(wǎng)站seo公司
  • 做外貿(mào)不能訪問國外網(wǎng)站怎么辦教程推廣優(yōu)化網(wǎng)站排名