計算機網路
基礎知識
OSI七層模型
第一層 物理層
- 主要定義了物理裝置的標準,比如說網線的型別、光纖的介面型別
- 進行數模轉換與模數轉換
- 傳輸的資料叫位元
- 網路卡就是工作在物理層
第二層 資料鏈路層
- 資料鏈路層定義瞭如何格式化資料以進行傳輸、控制如何對物理介質的訪問
- 提供資料錯誤檢測和糾正以確保資料傳輸的可靠性
- 資料鏈路層將位元資料組成了幀
- 交換機工作在資料鏈路層,對幀進行解碼,並根據幀中包含的資訊把資料傳送到正確的接收方
第三層 網路層
- 主要功能是將網路地址翻譯成對應的實體地址,並決定如何將資料從傳送方路由到接收方
- 網路層通過綜合考慮傳送優先權、網路擁塞程度、服務質量以及可選路由的花費來決定從一個網路中結點A到另一個網路中結點B的最佳路徑
- 路由器屬於網路層
- 網路層的資料稱為資料包
- IP協議也是在網路層
第四層 傳輸層
- 傳輸層解決了主機間的資料傳輸
- 傳輸協議同時進行流量控制或是基於接收方可接收資料的快慢程度規定適當的傳送速率,除此之外,傳輸層按照網路能處理的最大尺寸將較長的資料包進行強制分割,例如,以太坊無法接收大於1500位元組的資料包,傳送方結點的傳輸層將資料分割成較小的資料片,同時對每一資料片安排一序列號以便資料到達接收方結點的傳輸層時能以正確的順序重組,該過程即稱為排序
- 傳輸層中的重要協議:TCP、UDP
第五層 會話層
- 會話層的作用就是建立和管理應用程式之間的通訊
第六層 表示層
- 解決不同系統之間通訊語法的問題
- 在表示層資料將按照網路能理解的方案進行格式化,這種格式化也因所使用的網路的型別不同而不同
第七層 應用層
- 應用層規定傳送方和接收方必須使用一個固定長度的訊息頭,訊息頭必須使用某種固定的組成,而且訊息頭裡必須記錄訊息體的長度等等,以方便接收方能夠正確的解析傳送方傳送的資料
- 應用層旨在讓我們更方便的應用從網路中接收到的資料。
- 應用層中的重要協議:Http
TCP/IP概念層模型
說說TCP的三次握手
傳輸控制協議TCP簡介
- 面向連線的、可靠的、基於位元組流的傳輸層通訊協議
- 將應用層的資料流分割成報文段併傳送給目標節點的TCP層
- 資料包都有序號,對方收到則傳送ACK確認,未收到則重傳
- 使用奇偶校驗和來檢驗資料在傳輸過程中是否有誤(傳送和接受時都要計算校驗和)
TCP報文頭
Source Port | Destination Port (源埠、目的埠)(4位元組)
Sequence Number 報文段的序號(4位元組)
Acknowledgment Number 期望收到對方報文的第一個位元組的序號 (4位元組)
Offset(資料偏移,因為頭部有可選欄位,長度不固定,它指出TCP報文段的資料距離TCP報文的起始處有多遠) | Reserved(保留域,保留以後使用,目前都標為0) | TCP Flags | Window(滑動視窗的大小,用來告知傳送端/接收端的快取大小,以此控制傳送端傳送資料的速率,從而達到流量控制)
- TCP Flags
- URG:緊急指標標誌
- ACK:確認序號標誌
- PSH:push標誌
- RST:重置連線標誌
- SYN:同步序號,用於建立連線過程
- FIN:finish標誌,用於釋放連線
Checksum (檢驗和) 此校驗和是對整個的TCP報文段包括TCP頭部和TCP資料以十六位進行計算所得,由傳送端進行計算和儲存並由接收端進行驗證 | Urgent Pointer(緊急指標)只有當TCP Flags中的URG為1
時才有效,指出本報文段中的緊急資料的位元組數
TCP Options(variable、length、optional) 定義一些其他的可選引數
TCP三次握手流程
在TCP/IP協議中,TCP協議提供可靠的連線服務,採用三次握手建立一個連線
- 第一次握手:建立連線時,客戶端傳送SYN包(
syn = j
)到伺服器,並進入SYN_SEND狀態,等待伺服器確認 - 第二次握手:伺服器收到SYN包,必須確認客戶的SYN(
ack = j + 1
),同時自己也傳送一個SYN包(syn = k
),即SYN + ACK包,此時伺服器進入SYN_RECV狀態; - 第三次握手:客戶端收到伺服器的SYN + ACK包,向伺服器傳送確認包ACK(
ack = k + 1
),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。
為什麼需要三次握手才能建立起連線
- 為了初始化Sequence Number的初始值
通訊的雙方要互相通知對方自己的初始化的Sequence Number值,這個值要作為以後的資料通訊的序號,以保證應用層接收到的資料不會因為網路上的傳輸問題而亂序,即TCP會用這個序號來拼接資料,因此,在伺服器回發它的Sequence Number(即第二次握手)之後,客戶端還需要傳送確認報文給伺服器,告知伺服器說客戶端已經收到你的初始化Sequence Number了
首次握手的隱患
起因分析
Server 收到 Client 的 SYN ,回覆 SYN-ACK的時候未收到ACK確認
Server 不斷重試直至超時, Linux預設等到63秒才斷開連線(1s + 2s + 4s + 8s + 16s + 32s,重試五次,五次過後等32s,如果還沒有收到回應,那麼就結束,斷開連線)
- 針對SYN Flood的防護措施
- SYN佇列滿後,通過 tcp_syncookies引數回發SYN Cookie(源地址埠、目標地址埠和時間戳)
- 若為正常連線則Client會回發 SYN Cookie,直接建立接連
建立連線後,Client出現故障怎麼辦
- 保活機制
- 向對方傳送保活探測報文,如果未收到響應則繼續傳送
- 嘗試次數達到保活探測數仍未收到響應則中斷連線
談談TCP的四次揮手
TCP 採用四次揮手來釋放連線
- 第一次揮手:Client 傳送一個 FIN ,用來關閉 Client 到 Server 的資料傳送,Client進入 FIN_WAIT_1 狀態;
- 第二次揮手:Server 收到FIN後,傳送一個ACK給Client,確認序號為收到序號 + 1(與SYN相同,一個FIN佔用一個序號),Server進入 CLOSE_WAIT狀態;
- 第三次揮手:Server傳送一個FIN,用來關閉Server到Client的資料傳送,Server進入LAST_ACK狀態;
- 第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態(等待2MSL再CLOSED),接著傳送一個ACK給Server,確認序號為收到序號 + 1,Server進入CLOSED狀態,完成四次揮手。
為什麼會有TIME_WAIT狀態(為什麼會再等待2MSL才關閉連線)
- 確保有足夠的時間讓對方收到ACK包
- 避免新舊連線混淆
為什麼需要四次揮手才能斷開連線
- 因為TCP是全雙工的,傳送方和接收方都需要FIN報文和ACK報文,也就是說,傳送方和接收方各自需兩次揮手
伺服器出現大量CLOSED_WAIT狀態的原因
對方關閉socket連線,我方忙於讀或寫,沒有及時關閉連線
- 程式裡有BUG,檢查程式碼,特別是釋放資源的程式碼
- 檢查配置,特別是處理請求的執行緒配置
UDP
UDP的報文結構
Source Port 源埠 | Destination Port 目標埠
length 資料包長度 | Checksum 奇偶校驗值
data octets...(optional) 使用者資料
UDP的特點
- 面向非連線
- 不維護狀態,支援同時向多個客戶端傳輸相同的訊息
- 資料包報頭只有8個位元組,額外開銷較小
- 吞吐量只受限於資料生成速率、傳輸速率以及機器效能
- 盡最大努力交付,不保證可靠交付,不需要維持複雜的連結狀態表
- 面向報文,不對應用程式提交的報文資訊進行拆分或者合併
TCP和UDP的區別
- 面向連線 VS 無連線
- 可靠性:TCP較可靠,利用三次握手確認和重傳機制提供了可靠行保證,而UDP則可能會丟失,不能確認到底有沒有被接收
- 有序性:TCP利用序列號保證了訊息報的順序交付,到達時可能無序,但TCP最終會排序;而UDP不具備有序性
- 速度:TCP速度較慢,因為要建立連線,保證訊息的可靠性和有序性,需要做額外的很多事情;而UDP則更適合對速度比較敏感的應用,比如說線上視訊媒體,電視廣播等
- 量級:TCP屬於重量級,UDP屬於輕量級,體現在源資料的頭大小,TCP是20個位元組,而UDP是8個位元組
TCP的滑動視窗
-
RTT和RTO
- RTT:傳送一個資料包到收到對應的ACK所花費的時間
- RTO:重傳時間間隔
-
TCP使用滑動視窗做流量控制與亂序重排
-
TCP滑動視窗主要有兩個作用
- 保證TCP的可靠性
- 保證TCP的流控特性
-
TCP最基本的傳輸可靠性來源於確認重傳機制,TCP滑動視窗的可靠性也是建立在確認重傳基礎上的
HTTP
屬於應用層,HTTP協議是基於請求和響應模式的無狀態的應用層的協議,常基於TCP的連線方式
- 超文字傳輸協議HTTP主要特點
- 支援客戶/伺服器模式
- 簡單快速
- 靈活:HTTP允許傳輸任意型別的資料物件,正在傳輸的型別由
<Content-Type>
加以標記 - 無連線:限制每次連線只處理一個請求;HTTP1.1起,預設使用長連線,即伺服器需要等待一定時間後才能斷開連線,以保證連線特性。HTTP1.1預設開啟
keep-alive
,一定程度上彌補了HTTP1.0每次請求都要建立連線的缺點 - 無狀態:協議對事務處理沒有記憶能力,缺少狀態意味著如果後續處理需要前段資訊,則必須被重傳,這樣可能導致每次連線傳送的資料量增大,另一方面,在伺服器不需要先前資訊時,應答較快
HTTP請求結構
HTTP響應結構
請求/響應的步驟
- 客戶端連線到Web伺服器
- 傳送HTTP請求
- 伺服器接受請求並返回HTTP響應
- 釋放TCP連線:若連線模式為CLOESED,則伺服器主動關閉TCP連線,客戶端被動關閉連線,釋放TCP連線;若連線模式為
keep-alive
,則該連線會保持一段時間,在該時間內會繼續接收請求 - 客戶端瀏覽器解析HTML內容
在瀏覽器位址列鍵入URL,按下回車之後經歷的流程
- DNS解析
瀏覽器會依據URL逐層查詢DNS伺服器快取,解析URL中的域名所對應的IP地址,DNS快取從近到遠依次是瀏覽器快取、系統快取、路由器快取、IPS伺服器快取以及域名伺服器快取、頂級域名伺服器快取。從那個快取找到對應的IP,則直接返回,不再查詢後面的快取
- TCP連線
找到IP地址之後,會根據IP地址和對應埠與伺服器建立TCP連線(三次握手)
- 傳送HTTP請求
之後,瀏覽器會發出讀取檔案的HTTP請求,該請求將傳送給伺服器
- 伺服器處理請求並返回HTTP報文
緊接著,伺服器對瀏覽器請求做出響應並把對應的帶有HTML文字的HTTP響應報文傳送給瀏覽器
- 瀏覽器解析渲染頁面
瀏覽器收到了HTML並在顯示視窗內渲染它
- 連線結束
說說常見的HTTP狀態碼
五種可能的取值
- 1XX:指示資訊--表示請求已接收,繼續處理
- 2XX:成功--表示請求已被成功接收、理解、接受
- 3XX:重定向--要完成請求必須進行更進一步的操作
- 4XX:客戶端錯誤--請求有語法錯誤或請求無法實現
- 5XX:服務端錯誤--服務端未能實現合法的請求
常見狀態碼
- 200 OK:正常返回資訊
- 400 Bad Request:客戶端請求有語法錯誤,不能被伺服器所理解
- 401 Unauthorized:請求未經授權,這個狀態程式碼必須和WWW-Authenticate報頭域一起使用
- 403 Forbidden:伺服器收到請求,但是拒絕提供服務
- 404 Not Found:請求資源不存在;eg:輸入了錯誤的URL
- 500 Internal Server Error:伺服器發生不可預期的錯誤
- 503 Server Unavailable:伺服器當前不能處理客戶端的請求,一段時間後可能恢復正常
GET請求和POST請求的區別
從三個層面來解答
- HTTP報文層面:GET將請求資訊放在URL後面,請求資訊和URL之間以
?
隔開,請求資訊的格式為鍵值對;POST將請求資訊放在報文體中,想獲得請求資訊,必須解析報文;(安全性上兩者並沒有太多的區別,具體解決傳輸過程中的安全問題,還要靠HTTPS),由於GET中的請求資訊放置在URL中,因此是有長度限制的(URL本身沒有長度限制,但瀏覽器會對URL長度做出限制),POST中的請求資訊是放置在報文體中的,因此對資料長度是沒有限制的。 - 資料庫層面:GET符合冪等性和安全性,POST不符合(GET請求是做查詢操作的,因此不會改變資料庫中原有的資料,而POST請求會往資料庫中提交資料,因此不符合冪等性和安全性)
- 冪等性:對資料庫的一次操作或多次操作獲得的結果是一致的
- 安全性:對資料庫的操作沒有改變資料庫中的資料
- 其他層面:GET可以被快取、被儲存,而POST不行
- GET請求會儲存在瀏覽器的瀏覽記錄中,以GET請求的URL能夠儲存為瀏覽器書籤,而POST不具備上述功能,快取也是GET請求被廣泛應用的根本,在現代網路上,每天產生的請求數目是巨大的,並且其中絕大部分請求均為只讀請求,如果所有這些請求都要交由Web伺服器處理這無疑是巨大的資源浪費,因為GET請求是冪等的、安全的,因此絕大部分GET請求,通常超過90%都直接被CDN快取了,這能大大減少Web伺服器的負擔,而POST是非冪等的、有副作用的操作,所以必須交由Web伺服器處理
Cookie和Session的區別
因為HTTP是無狀態的,也就意味著我們每次訪問有登陸需求的頁面時,都要不厭其煩的輸入賬號和密碼,現實生活中並沒有出現這樣的情況,因為我們引入了某些機制,讓HTTP"具備''了狀態,其中的兩個便是Cookie和Session
Cookie簡介
Cookie技術是客戶端的解決方案
- 是由伺服器發給客戶端的特殊資訊(由伺服器發回客戶端時Cookie存放在HTTP響應頭中),以文字的形式存放在客戶端
- 客戶端再次請求的時候,會把Cookie回發(存放在HTTP請求頭中)
- 伺服器接收到後,會解析Cookie生成與客戶端相對應的內容.
Cookie的設定以及傳送過程
Session簡介
- 伺服器端的機制,在伺服器上儲存的資訊(類似於雜湊表的結構來儲存)
- 解析客戶端請求並操作session id,按需儲存狀態資訊
- 當程式需要為某個客戶端的請求建立一個session的時候,伺服器首先檢查這個客戶端的請求裡是否已包含了一個session標識,稱為session id,如果已包含一個session id,則說明以前已經為此客戶端建立過session,伺服器就按照session id把這個session檢索出來使用,如果檢索不到,就新建一個,如果客戶端請求不包含session id,則為此客戶端建立一個session,並且生成一個與此session相關的session id,此id將會在本次響應中回發給客戶端進行儲存
Session的實現方式
Cookie和Session的區別
- Cookie資料存放在客戶的瀏覽器上,Session資料存放在伺服器上
- Session相對於Cookie更安全
- 若考慮減輕伺服器負擔,應當使用Cookie
HTTP和HTTPS的區別
HTTPS簡介
- HTTPS是一種以計算機網路安全通訊為目的的傳輸協議
SSL (Security Sockets Layer,安全套接層)
- 為網路通訊推提供安全及資料完整性的一種安全協議
- 是作業系統對外的API,SSL3.0後更名為TLS(SSL位於TCP與各應用層之間)
- 採用身份驗證和資料加密保證網路通訊的安全和資料的完整性
加密的方式
- 對稱加密:加密和解密都使用同一個金鑰
- 非對稱加密:加密使用的金鑰和解密使用的金鑰是不相同的(公鑰和私鑰)
- 雜湊演算法:將任意長度的資訊轉換為固定長度的值,演算法不可逆
- 數字簽名:證明,某個訊息或者檔案是某人發出/認同的
在實際的執行中,人們發現,僅使用其中的某種加密方式並不能滿足生產要求,要麼非對稱加密效能過低,要麼對稱金鑰容易洩露,以此,HTTPS使用的是證照配合各種加密手段的方式做出了一套相對安全的組合拳
HTTPS資料傳輸流程
HTTPS在進行資料傳輸之前,會與網站伺服器和Web瀏覽器進行一次握手,在握手時,確定雙方的加密密碼資訊,具體流程如下
- 瀏覽器將支援的加密演算法資訊傳送給伺服器
- 伺服器選擇一套瀏覽器支援的加密演算法,以證照的形式回發瀏覽器
- 瀏覽器驗證證照合法性,並結合證照公鑰加密資訊傳送給伺服器 (如果證照受到瀏覽器信任,則在瀏覽器位址列會有標誌顯示,否則,就會顯示不受信的標識)
- 伺服器使用私鑰解密資訊,驗證雜湊,加密響應訊息回發瀏覽器
- 瀏覽器解密響應訊息,並對訊息進行驗真,之後進行加密互動資料
HTTP和HTTPS的區別
- HTTPS需要到CA申請證照,HTTP不需要
- HTTPS密文傳輸,HTTP明文傳輸
- 連線方式不同,HTTPS預設使用443埠,HTTP使用80埠
- HTTPS = HTTP + 加密 + 認證 + 完整性保護,較HTTP安全;(SSL是有狀態的)
HTTPS真的很安全嗎
那到未必
- 瀏覽器預設填充
http://
,請求需要進行跳轉,有被劫持的風險 - 可以使用HSTS(HTTP Strict Transport Security)優化,(目前正在推行中,並未成為主流)
Socket
Socket簡介
IP地址 + 協議 + 埠號可以唯一標識網路中的一個程式,能夠唯一標識網路中程式後,就可以利用Socket進行通訊了
Socket通訊流程
Socket相關的面試題
TCP實現
- 服務端
public class TCPServer {
public static void main(String[] args) throws IOException {
// 建立socket,並將socket繫結到65000埠
ServerSocket ss = new ServerSocket(65000);
// 死迴圈,使得socket一直等待並處理客戶端傳送過來的請求
while (true) {
// 監聽65000埠,直到客戶端返回連線資訊後才返回
Socket socket = ss.accept();
// 獲取客戶端的請求資訊後,執行相關的業務邏輯
new LengthCalculator(socket).start();
}
}
}
- 計算字串長度並列印
public class LengthCalculator extends Thread{
// 以socket為成員變數
private Socket socket;
public LengthCalculator(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 獲取socket的輸出流
OutputStream os = socket.getOutputStream();
// 獲取socket的輸入流
InputStream is = socket.getInputStream();
int ch = 0;
byte[] buff = new byte[1024];
// buff主要用來讀取輸入的內容,存成byte陣列,ch主要用來獲取陣列的長度
ch = is.read(buff);
// 將接收流的byte陣列轉換成字串,這裡獲取的內容是客戶端傳送過來的字串引數
String content = new String(buff, 0, ch);
System.out.println(content);
//往輸出流裡寫入獲得的字串長度,回發給客戶端
os.write(String.valueOf(content.length()).getBytes());
//不要忘記關閉輸入輸出流以及socket
is.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客戶端
public class TCPClient {
public static void main(String[] args) throws IOException {
// 建立socket,並指定連線的是本機的埠號為65000的伺服器socket
Socket socket = new Socket("127.0.0.1", 65000);
// 獲取輸出流
OutputStream os = socket.getOutputStream();
// 獲取輸入流
InputStream is = socket.getInputStream();
// 將要傳遞給Server的字串引數轉換成byte陣列,並將陣列寫入到輸出流中
os.write(new String("hello world").getBytes());
int ch = 0;
byte[] buff = new byte[1024];
// buff主要用來讀取輸入的內容,存成byte陣列,ch主要用來獲取讀取陣列的長度
ch = is.read(buff);
//將接收流的byte陣列轉換成字串,這裡是從服務端回發回來的字串引數的長度
String content = new String(buff, 0, ch);
System.out.println(content);
// 不要忘記關閉輸入輸出流以及socket
is.close();
os.close();
socket.close();
}
}
UDP實現
- 服務端
public class UDPServer {
public static void main(String[] args) throws Exception {
// 服務端接受客戶端傳送的資料包
DatagramSocket socket = new DatagramSocket(65001);
byte[] buff = new byte[100]; // 儲存從客戶端接收到的內容
DatagramPacket packet = new DatagramPacket(buff, buff.length);
// 接受從客戶端傳送過來的內容,並將內容封裝進DatagramPacket物件中
socket.receive(packet);
// 從DatagramPacket物件中獲取到真正儲存的資料
byte[] data = packet.getData();
// 將資料從二進位制轉換成字串形式
String content = new String(data, 0, packet.getLength());
System.out.println(content);
// 將要傳送給客戶端的資料轉換成二進位制
byte[] sendedContent = String.valueOf(content.length()).getBytes();
// 服務端給客戶端傳送資料包
// 從DatagramePacket物件中獲取到資料的來源地址與埠號
DatagramPacket packetToClient = new DatagramPacket(sendedContent,
sendedContent.length, packet.getAddress(), packet.getPort());
socket.send(packetToClient);
}
}
- 客戶端
public class UDPClient {
public static void main(String[] args) throws Exception {
// 客戶端發資料包給服務端
DatagramSocket socket = new DatagramSocket();
// 要傳送給服務端的資料
byte[] buff = "Hello world".getBytes();
// 將IP地址封裝成InetAddress物件
InetAddress address = InetAddress.getByName("127.0.0.1");
// 將要傳送給服務端的資料封裝成DatagramPacket物件,需要填上IP地址與埠號
DatagramPacket packet = new DatagramPacket(buff, buff.length, address, 65001);
// 傳送資料給服務端
socket.send(packet);
// 客戶端接受服務端傳送過來的資料包
byte[] data = new byte[100];
// 建立DatagramPacket物件用來儲存服務端傳送過來的資料
DatagramPacket receivedPacket = new DatagramPacket(data, data.length);
// 將接受到的資料儲存到DatagramPacket物件中
socket.receive(receivedPacket);
// 將服務端傳送過來的資料取出來並列印到控制檯
String content = new String(receivedPacket.getData(), 0,
receivedPacket.getLength());
System.out.println(content);
}
}