網絡協議及tcp協議詳解


問題來源:面試中面試官會看到你的簡歷上寫着熟悉網絡、http、tcp協議等,那你真的了解他嗎?今天它來了

 

一、網絡協議:

 

層次說明:
第七層:應用層(http
為操作系統或網絡應用程序提供訪問網絡服務的接口。
第六層:表示層
處理兩個通信系統中交換信息的表示方式。為上層用戶解決用戶信息的語法問題。包括數據格式交換、數據加密與解密、數據壓縮與終端類型的轉換。
第五層:會話層
在兩個節點之間建立端連接。為端系統的應用程序之間提供了對話控制機制。會話層不參與具體的傳輸,它提供包括訪問驗證和會話管理在內的建立和維護應用之間通信的機制。如服務器驗證用戶登錄便是由會話層完成的。
第四層:傳輸層(tcp
為會話層用戶提供一個端到端的可靠、透明和優化的數據傳輸服務機制。
第三層:網絡層
網絡層的任務就是選擇合適的網間路由和交換結點, 確保數據及時傳送。
第二層:數據鏈路層
在物理層提供比特流服務的基礎上,建立相鄰結點之間的數據鏈路,通過差錯控制提供數據幀在信道上無差錯的傳輸,並進行各電路上的動作系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:物理地址尋址、數據的成幀、流量控制、數據的檢錯、重發等。
第一層:物理層
物理層的主要功能是利用物理傳輸介質為數據鏈路層提供物理連接,以便透明的傳送比特流。
 
二、tcp協議:

 

三次握手(three-way handshake),建立TCP連接時會發生:
UserAgent > Server [SYN] 在么
Server > UserAgent [SYN, ACK] 在
UserAgent > Server [ACK] 知道了

四次揮手(four-way handshake),關閉TCP連接時會發生:
UserAgent > Server [FIN] 我要關閉連接了
Server > UserAgent [ACK] 知道了,等我發完包先
Server > UserAgent [FIN] 我也關閉連接了
UserAgent > Server [ACK] 好的,知道了

 

說說TCP三次握手的過程?

第一次握手:Client將標志位SYN置為1,隨機產生一個值seq=J,並將該數據包發送給Server,Client進入SYN_SENT狀態,等待Server確認。

第二次握手:Server收到數據包后由標志位SYN=1知道Client請求建立連接,Server將標志位SYN和ACK都置為1,ack=J+1,隨機產生一個值seq=K,並將該數據包發送給Client以確認連接請求,Server進入SYN_RCVD狀態。

第三次握手:Client收到確認后,檢查ack是否為J+1,ACK是否為1,如果正確則將標志位ACK置為1,ack=K+1,並將該數據包發送給Server,Server檢查ack是否為K+1,ACK是否為1,如果正確則連接建立成功,Client和Server進入ESTABLISHED狀態,完成三次握手,隨后Client與Server之間可以開始傳輸數據了。

TCP的報文格式是怎么樣的?
重要字段:

序號:Seq序號,占32位,用來標識從TCP源端向目的端發送的字節流,發起方發送數據時對此進行標記。

確認序號:Ack序號,占32位,只有ACK標志位為1時,確認序號字段才有效,Ack=Seq+1。標志位:共6個,即URG、ACK、PSH、RST、SYN、FIN等,具體含義如下:

  1. URG:緊急指針(urgent pointer)有效。
  2. ACK:確認序號有效。
  3. PSH:接收方應該盡快將這個報文交給應用層。
  4. RST:重置連接。
  5. SYN:發起一個新連接。
  6. FIN:釋放一個連接。

為什么要三次握手,兩次不可以嗎?
為了實現可靠數據傳輸,TCP協議的通信雙方都必須維護一個序列號,以標識發送出去的數據包中,哪些是已經被對方收到的。三次握手的過程即是通信雙方相互告知序列號起始值, 並確認對方已經收到了序列號起始值的必經步驟。如果只是兩次握手,至多只有連接發起方的起始序列號能被確認,另一方選擇的序列號則得不到確認.

如果已經建立了連接,但是客戶端突然出現故障了怎么辦?
客戶端如果出現故障,服務器不能一直等下去,白白浪費資源。服務端有一個保活計時器,每收到一次客戶端的請求后都會重新復位這個計時器,時間通常是設置為2小時,若兩小時還沒有收到客戶端的任何數據,服務器就會發送一個探測報文段,以后每隔75秒鍾發送一次。若一連發送10個探測報文仍然沒反應,服務器就認為客戶端出了故障,就關閉連接。

再說說TCP的四次揮手的過程?
由於TCP連接時全雙工的,每個方向都必須要單獨進行關閉,這一原則是當一方完成數據發送任務后,發送一個FIN來終止這一方向的連接,收到一個FIN只是意味着這一方向上沒有數據流動了,即不會再收到數據了,但是在這個TCP連接上仍然能夠發送數據,直到這一方向也發送了FIN。首先進行關閉的一方將執行主動關閉,而另一方則執行被動關閉。

  1. 第一次揮手:Client發送一個FIN,用來關閉Client到Server的數據傳送,Client進入FIN_WAIT_1狀態。
  2. 第二次揮手:Server收到FIN后,發送一個ACK給Client,確認序號為收到序號+1(與SYN相同,一個FIN占用一個序號),Server進入CLOSE_WAIT狀態。
  3. 第三次揮手:Server發送一個FIN,用來關閉Server到Client的數據傳送,Server進入LAST_ACK狀態。
  4. 第四次揮手:Client收到FIN后,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號為收到序號+1,Server進入CLOSED狀態,完成四次揮手。

為什么要四次揮手,三次不可以嗎?
因為當Server端收到Client端的SYN連接請求報文后,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連接時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,"你發的FIN報文我收到了"。只有等到我Server端所有的報文都發送完了,我才能發送FIN報文,因此不能一起發送。故需要四步握手。

 

三、不使用API手動實現一個http的類:

服務端步驟:

|- WSAStartup函數初始化
|- 創建Socket
|- 用bind指定對象
|- listen設置監聽
|- accept接收請求
|- send發送會話
|- closesocket關閉socket

 

客戶端步驟:

|- WSAStartup函數初始化
|- 創建Socket
|- connect請求連接
|- send發送會話
|- closesocket關閉socket

 

服務端代碼:

#include "pch.h"
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
//定義長度變量
int send_len = 0;
int recv_len = 0;
int len = 0;
//定義發送緩沖區和接受緩沖區
char send_buf[100];
char recv_buf[100];
//定義服務端套接字,接受請求套接字
SOCKET s_server;
SOCKET s_accept;
//服務端地址客戶端地址
SOCKADDR_IN server_addr;
SOCKADDR_IN accept_addr;
initialization();
//填充服務端信息
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(5010);
//創建套接字
s_server = socket(AF_INET, SOCK_STREAM, 0);
if (bind(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
cout << "套接字綁定失敗!" << endl;
WSACleanup();
}
else {
cout << "套接字綁定成功!" << endl;
}
//設置套接字為監聽狀態
if (listen(s_server, SOMAXCONN) < 0) {
cout << "設置監聽狀態失敗!" << endl;
WSACleanup();
}
else {
cout << "設置監聽狀態成功!" << endl;
}
cout << "服務端正在監聽連接,請稍候...." << endl;
//接受連接請求
len = sizeof(SOCKADDR);
s_accept = accept(s_server, (SOCKADDR *)&accept_addr, &len);
if (s_accept == SOCKET_ERROR) {
cout << "連接失敗!" << endl;
WSACleanup();
return 0;
}
cout << "連接建立,准備接受數據" << endl;
//接收數據
while (1) {
recv_len = recv(s_accept, recv_buf, 100, 0);
if (recv_len < 0) {
cout << "接受失敗!" << endl;
break;
}
else {
cout << "客戶端信息:" << recv_buf << endl;
}
cout << "請輸入回復信息:";
cin >> send_buf;
send_len = send(s_accept, send_buf, 100, 0);
if (send_len < 0) {
cout << "發送失敗!" << endl;
break;
}
}
//關閉套接字
closesocket(s_server);
closesocket(s_accept);
//釋放DLL資源
WSACleanup();
return 0;
}
void initialization() {
//初始化套接字庫
WORD w_req = MAKEWORD(2, 2);//版本號
WSADATA wsadata;
int err;
err = WSAStartup(w_req, &wsadata);
if (err != 0) {
cout << "初始化套接字庫失敗!" << endl;
}
else {
cout << "初始化套接字庫成功!" << endl;
}
//檢測版本號
if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
cout << "套接字庫版本號不符!" << endl;
WSACleanup();
}
else {
cout << "套接字庫版本正確!" << endl;
}
//填充服務端地址信息

}

 

客戶端代碼:

 
           

#include "pch.h"
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
//定義長度變量
int send_len = 0;
int recv_len = 0;
//定義發送緩沖區和接受緩沖區
char send_buf[100];
char recv_buf[100];
//定義服務端套接字,接受請求套接字
SOCKET s_server;
//服務端地址客戶端地址
SOCKADDR_IN server_addr;
initialization();
//填充服務端信息
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(1234);
//創建套接字
s_server = socket(AF_INET, SOCK_STREAM, 0);
if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
cout << "服務器連接失敗!" << endl;
WSACleanup();
}
else {
cout << "服務器連接成功!" << endl;
}

//發送,接收數據
while (1) {
cout << "請輸入發送信息:";
cin >> send_buf;
send_len = send(s_server, send_buf, 100, 0);
if (send_len < 0) {
cout << "發送失敗!" << endl;
break;
}
recv_len = recv(s_server, recv_buf, 100, 0);
if (recv_len < 0) {
cout << "接受失敗!" << endl;
break;
}
else {
cout << "服務端信息:" << recv_buf << endl;
}

}
//關閉套接字
closesocket(s_server);
//釋放DLL資源
WSACleanup();
return 0;
}
void initialization() {
//初始化套接字庫
WORD w_req = MAKEWORD(2, 2);//版本號
WSADATA wsadata;
int err;
err = WSAStartup(w_req, &wsadata);
if (err != 0) {
cout << "初始化套接字庫失敗!" << endl;
}
else {
cout << "初始化套接字庫成功!" << endl;
}
//檢測版本號
if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
cout << "套接字庫版本號不符!" << endl;
WSACleanup();
}
else {
cout << "套接字庫版本正確!" << endl;
}
//填充服務端地址信息

}

 

參考:

https://blog.csdn.net/qq_27923041/article/details/83857964?utm_source=distribute.pc_relevant.none-task

 

歡迎大家留言交流,一塊成長進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM