tcp socket通信的基本過程:socket通信原理

一個完整的HTTP請求的過程

此舉例為拋磚引玉,引導大家進入思考狀態。

當你按輸入www.baidu.com ,瀏覽器接收到這個消息之後,瀏覽器根據自己的演算法識別出你要訪問的URL,為您展示出來搜索頁面和廣告,那麼這些經歷了哪些過程呢?

大致過程如下:

  • (1)瀏覽器查詢 DNS,獲取域名對應的IP地址; 具體過程包括瀏覽器搜索自身的DNS緩存、搜索操作系統的DNS緩存、讀取本地的Host文件和向本地DNS服 務器進行查詢等。
  • (2)瀏覽器獲得域名對應的IP地址以後,瀏覽器向伺服器請求建立鏈接,發起三次握手;
  • (3)TCP/IP鏈接建立起來後,瀏覽器向伺服器發送HTTP請求;
  • (4)伺服器接收到這個請求,並根據路徑參數映射到特定的請求處理器進行處理,並將處理結果及相應的視圖返回給瀏覽器;
  • (5)瀏覽器解析並渲染視圖,若遇到對js文件、css文件及圖片等靜態資源的引用,則重複上述步驟並向伺服器請求這些資源;
  • (6)瀏覽器根據其請求到的資源、數據渲染頁面,最終向用戶呈現一個完整的頁面。

下面,我們從底到上來一層層理解這個問題。

網路參考模型

開放式系統互聯通信參考模型(英語:Open System Interconnection Reference Model,縮寫:OSI;簡稱為OSI模型)是一種概念模型,由國際標準化組織提出,一個試圖使各種計算機在世界範圍內互連為網路的標準框架。定義於ISO/IEC 7498-1。(摘自維基百科)

7應用層 application layer例如HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP、TLS
6表示層 presentation layer例如XDR、ASN.1、SMB、AFP、NCP
5會話層 session layer例如ASAP、ISO 8327 / CCITT X.225、RPC、NetBIOS、ASP、IGMP、Winsock、BSD sockets
4傳輸層 transport layer例如TCP、UDP、RTP、SCTP、SPX、ATP、IL
3網路層 network layer例如IP、ICMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、X.25
2數據鏈路層 data link layer例如乙太網、令牌環、HDLC、幀中繼、ISDN、ATM、IEEE 802.11、FDDI、PPP
1物理層 physical layer例如線路、無線電、光纖

通常人們認為OSI模型的最上面三層(應用層、表示層和會話層)在TCP/IP組中是一個應用層。

由於TCP/IP有一個相對較弱的會話層,由TCP和RTP下的打開和關閉連接組成,並且在TCP和UDP下的各種應用提供不同的埠號,這些功能能夠被單個的應用程序(或者那些應用程序所使用的庫)增加。與此相似的是,IP是按照將它下面的網路當作一個黑盒子的思想設計的,這樣在討論TCP/IP的時候就可以把它當作一個獨立的層。

TCP/IP 參考模型

4應用層 application layer例如HTTP、FTP、DNS (如BGP和RIP這樣的路由協議,儘管由於各種各樣的原因它們分別運行在TCP和UDP上,仍然可以將它們看作網路層的一部分)
3傳輸層 transport layer例如TCP、UDP、RTP、SCTP (如OSPF這樣的路由協議,儘管運行在IP上也可以看作是網路層的一部分)
2網路互連層 internet layer對於TCP/IP來說這是網際網路協議(IP) (如ICMP和IGMP這樣的必須協議儘管運行在IP上,也仍然可以看作是網路互連層的一部分;ARP不運行在IP上)
1網路訪問(鏈接)層 Network Access(link) layer例如乙太網、Wi-Fi、MPLS等。

下面一張圖更有助於你的理解

HTTP—TCP/IP—SOCKET理解及淺析

HTTP 協議與 TCP/IP 協議

**HTTP 是 TCP/IP 參考模型中應用層的其中一種實現。**HTTP 協議的網路層基於 IP 協議,傳輸層基於 TCP 協議:HTTP 協議是基於 TCP/IP 協議的應用層協議。

TCP/IP 協議需要向程序員提供可編程的 API,該 API 就是 Socket,它是對 TCP/IP 協議的一個重要的實現,幾乎所有的計算機系統都提供了對 TCP/IP 協議族的 Socket 實現。

Socket是進程通訊的一種方式,即調用這個網路庫的一些API函數實現分布在不同主機的相關進程之間的數據交換。

  • 流格式套接字(SOCK_STREAM) 流格式套接字(Stream Sockets)也叫「面向連接的套接字」,它基於 TCP 協議,在代碼中使用 SOCK_STREAM 表示。
  • 數據報格式套接字(SOCK_DGRAM) 數據報格式套接字(Datagram Sockets)也叫「無連接的套接字」,基於 UDP 協議,在代碼中使用 SOCK_DGRAM 表示。 TCP與UDP 協議區別與優劣勢 TCP 是面向連接的傳輸協議,建立連接時要經過三次握手,斷開連接時要經過四次握手,中間傳輸數據時也要回復 ACK 包確認,多種機制保證了數據能夠正確到達,不會丟失或出錯。 UDP 是非連接的傳輸協議,沒有建立連接和斷開連接的過程,它只是簡單地把數據丟到網路中,也不需要 ACK 包確認。 如果只考慮可靠性,TCP 的確比 UDP 好。但 UDP 在結構上比 TCP 更加簡潔,不會發送 ACK 的應答消息,  也不 會給數據包分配 Seq 序號,所以 UDP 的傳輸效率有時會比 TCP 高出很多,編程中實現 UDP 也比   TCP 簡單。 與 UDP 相比,TCP 的生命在於流控制,這保證了數據傳輸的正確性。

最後需要說明的是:TCP 的速度無法超越 UDP,但在收發某些類型的數據時有可能接近 UDP。例如,每次交換的數據量越大,TCP 的傳輸速率就越接近於 UDP。

分享更多網路底層原理知識點,內容包括Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等。後台私信【架構】獲取

HTTP—TCP/IP—SOCKET理解及淺析

TCP/IP 協議、HTTP 協議和 Socket 有什麼區別?

從包含範圍來看,它們的繼承關係是這樣的:

HTTP—TCP/IP—SOCKET理解及淺析

從橫向來看,它們的繼承關係是這樣的:

HTTP—TCP/IP—SOCKET理解及淺析

關於TCP/IP和HTTP協議的關係,有一段比較容易理解的介紹:

  我們在傳輸數據時,可以只使用(傳輸層)TCP/IP協議,但是那樣的話,如果沒有應用層,便無法識別數據內容,如果想要使傳輸的數據有意義,則必須使用到應用層協議,應用層協議有很多,比如HTTP、FTP、TELNET等,也可以自己定義應用層協議。WEB使用HTTP協議作應用層協議,以封裝HTTP文本信息,然後使用TCP/IP做傳輸層協議將它發到網路上。

Socket是什麼呢,實際上Socket是對TCP/IP協議的封裝,Socket本身並不是協議,而是一個調用介面(API),通過Socket,我們才能使用TCP/IP協議。

TCP/IP只是一個協議棧,就像操作系統的運行機制一樣,必須要具體實現,同時還要提供對外的操作介面。這個就像操作系統會提供標準的編程介面,比如win32編程介面一樣,TCP/IP也要提供可供程序員做網路開發所用的介面,這就是Socket編程介面。」

TCP/IP 和 HTTP 的數據結構

HTTP 作為 TCP/IP 參考模型的應用層,把 HTTP 放到 TCP/IP 參考模型中,它們的繼承結構是這樣的:

HTTP—TCP/IP—SOCKET理解及淺析

在 TCP/IP 參考模型中它們的整體的數據結構是:IP 作為乙太網的直接底層,IP 的頭部和數據合起來作為乙太網的數據,同樣的 TCP/UDP 的頭部和數據合起來作為 IP 的數據,HTTP 的頭部和數據合起來作為 TCP/UDP 的數據。

HTTP—TCP/IP—SOCKET理解及淺析

IP 的數據結構和交互流程

我們都知道在一個成功的 HTTP 請求中,服務端可以在一個請求中獲取到客戶端 IP 地址,也可以獲取到客戶端請求的主機的 IP 地址。然而這是怎麼做到的呢?這就有賴於 IP 協議了,在 IP 協議中規定了,IP 的頭部必須包含源 IP 地址和目的 IP 地址,這也是為什麼在 TCP/IP 參考模型中IP 處在網路互聯層,其中一個原因就是可以定位服務端地址和客戶端地址,我們來看一下 IP 的數據結構:

HTTP—TCP/IP—SOCKET理解及淺析

可以很清晰的看到源 IP 地址和目的 IP 地址,在 IP 的頭部各占 32 位,而 IPV4 的 IP 地址是用點式十進位表示的,例如:192.168.1.1,在 IP 頭部用二進位表示的話,剛好是 4 個位元組 32 位。

32 位可以表示的 IP 地址是有限的,使用了 IP 地址轉換技術 NAT。例如 ABC 三個小區的所有設備可能公用了一個公網 IP,通過 NAT 技術分給每一戶一個私有 IP 地址,大家在小區內交流時可能使用的是私有 IP 地址,但是向外交流時就用公網 IP。

TCP 的數據結構和交互流程

我們通常說的 HTTP 的 3 次握手和 4 次揮手都是由 TCP 來完成的,其實這都沒 HTTP 什麼事,但是有不少人喜歡這麼說,嚴格來說我們應該說 TCP 的 3 次握手 4 次揮手。要搞清楚 TCP 的交互流程,首先要清楚 TCP 的數據結構,接下來我們來看一下 TCP 的數據結構:

HTTP—TCP/IP—SOCKET理解及淺析

上述 TCP 的數據結構圖對於後面理解 HTTP 的交互流程非常重要,我們要記住 5 個關鍵的位置:

SYN:建立連接標識 ACK:響應標識 FIN:斷開連接標識 seq:seq number,發送序號 ack:ack number,響應序號

服務端應用啟動後,會在指定埠監聽客戶端的連接請求,當客戶端嘗試創建一個到服務端指定埠的 TCP 連接,服務端收到請求後接受數據並處理完業務後,會向客戶端作出響應,客戶端收到響應後接受響應數據,然後斷開連接,一個完整的請求流程就完成了。這樣的一個完整的 TCP 的生命周期會經歷以下 4 個步驟

1,建立 TCP 連接,3 次握手

客戶端發送SYN, seq=x,進入 SYN_SEND 狀態

服務端回應SYN, ACK, seq=y, ack=x+1,進入 SYN_RCVD 狀態

客戶端回應ACK, seq=x+1, ack=y+1,進入 ESTABLISHED 狀態,服務端收到後進入 ESTABLISHED 狀態 2,進行數據傳輸

客戶端發送ACK, seq=x+1, ack=y+1, len=m

服務端回應ACK, seq=y+1, ack=x+m+1, len=n

客戶端回應ACK, seq=x+m+1, ack=y+n+1

3,斷開 TCP 連接, 4 次揮手

主機 A 發送FIN, ACK, seq=x+m+1, ack=y+n+1,進入 FNI_WAIT_1 狀態

主機 B 回應ACK, seq=y+n+1, ack=x+m+1,進入 CLOSE_WAIT 狀態,主機 A 收到後 進入 FIN_WAIT_2 狀態

主機 B 發送FIN, ACK, seq=y+n+1, ack=x+m+1,進入 LAST_ACK 狀態

主機 A 回應ACk, seq=x+m+1, ack=y+n+1,進入 TIME_WAIT 狀態,等待主機 B 可能要求重傳 ACK 包,主機 B 收到後關閉連接,進入 CLOSED 狀態或者要求主機 A 重傳 ACK,客戶端在一定的時間內沒收到主機 B 重傳 ACK 包的要求後,斷開連接進入 CLOSED 狀態

為什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?

雖然按道理,四個報文都發送完畢,我們可以直接進入CLOSE狀態了,但是我們必須假設網路是不可靠的,一切都可能發生,比如有可能最後一個ACK丟失。所以TIME_WAIT狀態是用來重發可能丟失的ACK報文。

HTTP—TCP/IP—SOCKET理解及淺析

客戶端與服務端建立連接、傳輸數據和斷開連接等全靠這幾個標識,比如 SYN 也可以被用來作為 DOS 攻擊的一個手段,FIN 可以用來掃描服務端指定埠。

HTTP 的數據結構

Socket 是 TCP/IP 的可編程 API,HTTP 的可編程 API 的實現要依賴 Socket。HTTP 是超文本傳輸協議,HTTP 的頭和數據看起來更加直觀,在大多數情況下,它們都是字元或者字元串,所以對於大多數人來說理解 HTTP 的頭和數據格式顯得很簡單。確實,HTTP 的數據格式理解起來非常容易,上部分是頭,下部分是身體。

HTTP 的請求時的數據結構和響應時的數據結構整體上是一樣的,但是有一些細微的區別,我們先來看一下 HTTP 請求時的數據結構:

HTTP—TCP/IP—SOCKET理解及淺析

HTTP 響應時的數據結構:

HTTP—TCP/IP—SOCKET理解及淺析

現在我們使用谷歌瀏覽器請求某度,按下F12,來對比理解上述結構圖,下面是請求某度

HTTP—TCP/IP—SOCKET理解及淺析

我們就可以簡單的理解 HTTP 的數據結構了。

分享更多網路底層原理知識點,內容包括Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等。後台私信【架構】獲取

Linux下的socket演示程序

下面用最基礎的Socket來進行服務端與客戶端的交互,讓你理解的更為清晰。

介面詳解

方法名用途
socket():創建socket
bind():綁定socket到本地地址和埠,通常由服務端調用
listen():TCP專用,開啟監聽模式
accept():TCP專用,伺服器等待客戶端連接,一般是阻塞態
connect():TCP專用,客戶端主動連接伺服器
send():TCP專用,發送數據
recv():TCP專用,接收數據
sendto():UDP專用,發送數據到指定的IP地址和埠
recvfrom():UDP專用,接收數據,返回數據遠端的IP地址和埠
close():關閉socket

基於TCP協議實現CS端

使用Socket進行網路通信的過程

① 伺服器程序將一個套接字綁定到一個特定的埠,並通過此套接字等待和監聽客戶的連接請求。

② 客戶程序根據伺服器程序所在的主機和埠號發出連接請求。

③ 如果一切正常,伺服器接受連接請求。並獲得一個新的綁定到不同埠地址的套接字。

④ 客戶和伺服器通過讀、寫套接字進行通訊。

HTTP—TCP/IP—SOCKET理解及淺析

客戶機/伺服器模式

在TCP/IP網路應用中,通信的兩個進程間相互作用的主要模式是客戶機/伺服器模式*(client/server),即客戶像服務其提出請求,伺服器接受到請求後,提供相應的服務。

伺服器:

(1)首先伺服器方要先啟動,打開一個通信通道並告知本機,它願意在某一地址和埠上接收客戶請求

(2)等待客戶請求到達該埠

(3)接收服務請求,處理該客戶請求,服務完成後,關閉此新進程與客戶的通信鏈路,並終止

(4)返回第二步,等待另一個客戶請求

(5)關閉伺服器

客戶方:

(1)打開一個通信通道,並連接到伺服器所在的主機特定的埠

(2)向伺服器發送請求,等待並接收應答,繼續提出請求

(3)請求結束後關閉通信信道並終止

具體實現,新建服務端socket_server_tcp.c

具體代碼如下: socket_server_tcp.c

//
// Created by android on 19-8-9.
//
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>

#define PORT 3040        //埠號
#define BACKLOG 5    //最大監聽數

int main() {
    int iSocketFD = 0;  //socket句柄
    int iRecvLen = 0;   //接收成功後的返回值
    int new_fd = 0;    //建立連接後的句柄
    char buf[4096] = {0}; //
    struct sockaddr_in stLocalAddr = {0}; //本地地址信息結構圖,下面有具體的屬性賦值
    struct sockaddr_in stRemoteAddr = {0}; //對方地址信息
    socklen_t socklen = 0;

    iSocketFD = socket(AF_INET, SOCK_STREAM, 0); //建立socket SOCK_STREAM代表以tcp方式進行連接
    if (0 > iSocketFD) {
        printf("創建socket失敗!n");
        return 0;
    }

    stLocalAddr.sin_family = AF_INET;  /*該屬性表示接收本機或其他機器傳輸*/
    stLocalAddr.sin_port = htons(PORT); /*埠號*/
    stLocalAddr.sin_addr.s_addr = htonl(INADDR_ANY); /*IP,括弧內容表示本機IP*/

    //綁定地址結構體和socket
    if (0 > bind(iSocketFD, (void *) &stLocalAddr, sizeof(stLocalAddr))) {
        printf("綁定失敗!n");
        return 0;
    }

    //開啟監聽 ,第二個參數是最大監聽數
    if (0 > listen(iSocketFD, BACKLOG)) {
        printf("監聽失敗!n");
        return 0;
    }

    printf("iSocketFD: %dn", iSocketFD);
    //在這裡阻塞知道接收到消息,參數分別是socket句柄,接收到的地址信息以及大小 
    new_fd = accept(iSocketFD, (void *) &stRemoteAddr, &socklen);
    if (0 > new_fd) {
        printf("接收失敗!n");
        return 0;
    } else {
        printf("接收成功!n");
        //發送內容,參數分別是連接句柄,內容,大小,其他信息(設為0即可) 
        send(new_fd, "這是伺服器接收成功後發回的信息!", sizeof("這是伺服器接收成功後發回的信息!"), 0);
    }

    printf("new_fd: %dn", new_fd);
    iRecvLen = recv(new_fd, buf, sizeof(buf), 0);
    if (0 >= iRecvLen)    //對端關閉連接 返回0
    {
        printf("對端關閉連接或者接收失敗!n");
    } else {
        printf("buf: %sn", buf);
    }

    close(new_fd);
    close(iSocketFD);

    return 0;
}

新建客戶端端socket_client_tcp.c socket_client_tcp.c

//
// Created by android on 19-8-9.
//

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>

#define PORT 3040            //目標地址埠號
#define ADDR "10.6.191.177" //目標地址IP

int main() {
    int iSocketFD = 0; //socket句柄
    unsigned int iRemoteAddr = 0;
    struct sockaddr_in stRemoteAddr = {0}; //對端,即目標地址信息
    socklen_t socklen = 0;
    char buf[4096] = {0}; //存儲接收到的數據

    iSocketFD = socket(AF_INET, SOCK_STREAM, 0); //建立socket
    if (0 > iSocketFD) {
        printf("創建socket失敗!n");
        return 0;
    }

    stRemoteAddr.sin_family = AF_INET;
    stRemoteAddr.sin_port = htons(PORT);
    inet_pton(AF_INET, ADDR, &iRemoteAddr);
    stRemoteAddr.sin_addr.s_addr = iRemoteAddr;

    //連接方法: 傳入句柄,目標地址,和大小
    if (0 > connect(iSocketFD, (void *) &stRemoteAddr, sizeof(stRemoteAddr))) {
        printf("連接失敗!n");
        //printf("connect failed:%d",errno);//失敗時也可列印errno
    } else {
        printf("連接成功!n");
        recv(iSocketFD, buf, sizeof(buf), 0); ////將接收數據打入buf,參數分別是句柄,儲存處,最大長度,其他信息(設為0即可)。 
        printf("Received:%sn", buf);
    }

    close(iSocketFD);//關閉socket
    return 0;
}

下面是我的編譯及運行效果:

HTTP—TCP/IP—SOCKET理解及淺析

編譯命令如下:

 gcc -o server socket_server_tcp.c
 gcc -o client socket_client_tcp.c
 #運行命令
  ./server  #首先啟動
  ./client #次之啟動

基於UDP協議實現CS端

**基於UDP(面向無連接)的socket編程——**數據報式套接字(SOCK_DGRAM) 網路間通信AF_INET,典型的TCP/IP四型模型的通信過程

伺服器:(多線程的【每10秒會列印一行#號】 與 循環監聽) socket_server_udp.c

//
// Created by android on 19-8-9.
//


#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>

void * test(void *pvData)
{
    while(1)
    {
        sleep(5);
        printf("################################n");
    }
    return NULL;
}

int main(void)
{
    pthread_t stPid = 0;
    int iRecvLen = 0;
    int iSocketFD = 0;
    char acBuf[4096] = {0};
    struct sockaddr_in stLocalAddr = {0};

    struct sockaddr_in stRemoteAddr = {0};
    socklen_t iRemoteAddrLen = 0;

    /* 創建socket */
    iSocketFD = socket(AF_INET, SOCK_DGRAM, 0);
    if(iSocketFD < 0)
    {
        printf("創建socket失敗!n");
        return 0;
    }

    /* 填寫地址 */
    stLocalAddr.sin_family = AF_INET;
    stLocalAddr.sin_port   = htons(12345);
    stLocalAddr.sin_addr.s_addr = 0;

    /* 綁定地址 */
    if(0 > bind(iSocketFD, (void *)&stLocalAddr, sizeof(stLocalAddr)))
    {
        printf("綁定地址失敗!n");
        close(iSocketFD);
        return 0;
    }
    pthread_create(&stPid, NULL, test, NULL);   //實現了多線程

    while(1)     //實現了循環監聽
    {
        iRecvLen = recvfrom(iSocketFD, acBuf, sizeof(acBuf), 0, (void *)&stRemoteAddr, &iRemoteAddrLen);
        printf("iRecvLen: %dn", iRecvLen);
        printf("acBuf:%sn", acBuf);
    }
    close(iSocketFD);

    return 0;
}

客戶端: socket_client_udp.c

//
// Created by android on 19-8-9.
//

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>

int main(void)
{
    int iRecvLen = 0;
    int iSocketFD = 0;
    int iRemotAddr = 0;
    char acBuf[4096] = {0};
    struct sockaddr_in stLocalAddr = {0};

    struct sockaddr_in stRemoteAddr = {0};
    socklen_t iRemoteAddrLen = 0;

    /* 創建socket */
    iSocketFD = socket(AF_INET, SOCK_DGRAM, 0);
    if(iSocketFD < 0)
    {
        printf("創建socket失敗!n");
        return 0;
    }

    /* 填寫服務端地址 */
    stLocalAddr.sin_family = AF_INET;
    stLocalAddr.sin_port   = htons(12345);
    inet_pton(AF_INET, "10.6.191.177", (void *)&iRemotAddr);
    stLocalAddr.sin_addr.s_addr = iRemotAddr;
    iRecvLen = sendto(iSocketFD, "這是一個測試字元串", strlen("這是一個測試字元串"), 0, (void *)&stLocalAddr, sizeof(stLocalAddr));

    close(iSocketFD);
    return 0;
}

測試:

1、編譯伺服器:因為有多線程,所以伺服器端進程要進行pthread編譯

gcc socket_server_udp.c -pthread -g -o server_udp #客戶端和上方相同
複製代碼

執行結果如下:

右下為客戶端重複執行

HTTP—TCP/IP—SOCKET理解及淺析

伺服器端有主線程和輔線程,主線程,列印客戶端發送的請求;輔線程每隔5秒鐘列印一排#號。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/225642.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-09 14:45
下一篇 2024-12-09 14:45

相關推薦

發表回復

登錄後才能評論