一個完整的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 協議
**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等等。後台私信【架構】獲取

TCP/IP 協議、HTTP 協議和 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 參考模型中,它們的繼承結構是這樣的:

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

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

可以很清晰的看到源 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 的數據結構:

上述 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報文。

客戶端與服務端建立連接、傳輸數據和斷開連接等全靠這幾個標識,比如 SYN 也可以被用來作為 DOS 攻擊的一個手段,FIN 可以用來掃描服務端指定端口。
HTTP 的數據結構
Socket 是 TCP/IP 的可編程 API,HTTP 的可編程 API 的實現要依賴 Socket。HTTP 是超文本傳輸協議,HTTP 的頭和數據看起來更加直觀,在大多數情況下,它們都是字符或者字符串,所以對於大多數人來說理解 HTTP 的頭和數據格式顯得很簡單。確實,HTTP 的數據格式理解起來非常容易,上部分是頭,下部分是身體。
HTTP 的請求時的數據結構和響應時的數據結構整體上是一樣的,但是有一些細微的區別,我們先來看一下 HTTP 請求時的數據結構:

HTTP 響應時的數據結構:

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

我們就可以簡單的理解 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進行網絡通信的過程
① 服務器程序將一個套接字綁定到一個特定的端口,並通過此套接字等待和監聽客戶的連接請求。
② 客戶程序根據服務器程序所在的主機和端口號發出連接請求。
③ 如果一切正常,服務器接受連接請求。並獲得一個新的綁定到不同端口地址的套接字。
④ 客戶和服務器通過讀、寫套接字進行通訊。

客戶機/服務器模式
在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;
}
下面是我的編譯及運行效果:

編譯命令如下:
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 #客戶端和上方相同
複製代碼執行結果如下:
右下為客戶端重複執行

服務器端有主線程和輔線程,主線程,打印客戶端發送的請求;輔線程每隔5秒鐘打印一排#號。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/225642.html
微信掃一掃
支付寶掃一掃