Java後端面試系列-計算機網絡篇
- 1 OSI七層概念模型
- 2 TCP/IP
- 3 說說TCP的三次握手
- 3.1 傳輸控制協議TCP簡介
- 3.2 TCP報文頭
- 3.3 TCP Flags
- 3.4 三次握手流程
- 3.5 爲什麼需要三次握手才能建立連接
- 3.6 首次握手的隱患——SYN超時
- 3.7 建立連接後,Clinet出現故障怎麼辦
- 4 談談TCP的四次揮手
- 5 說說UDP
- 8 HTTP協議
- 9 在瀏覽器地址欄鍵入URL,按下回車之後經歷的流程
- 10 HTTP狀態碼
- 11 GET請求和POST請求的區別
- 12 Cookie和Session
- 13 HTTPS
- 13.1 HTTPS簡介
- 13.2 SSL(Security Sockets Layer,安全套接層)
- 13.3 加密方式
- 13.4 HTTPS數據傳輸流程
- 13.5 HTTP和HTTPS的區別
- 13.6 HTTPS真的很安全嗎
- 14 Socket
1 OSI七層概念模型
1.1 物理層
目的:解決兩臺物理機的通信需求,傳遞的是比特流,也就是010101...二進制數據,先轉
成電流的強弱進行傳輸,達到後轉成010101...(也就是所謂的數模轉換和模數轉換)。網卡
就是工作在這一層。
1.2 數據鏈路層
在傳輸比特流的過程中,會產生錯傳、數據傳輸不完整等情況。針對這些情況,數據鏈路層
定義瞭如何格式化數據,控制對物理介質的訪問,還提供錯誤檢測和糾正以保證數據傳輸的可
靠性,其中將比特流組成了幀。交換機就是工作在這一層,對幀解碼,並將幀中的數據發送到
正確的接收方。
1.3 網絡層
主要工作是將網絡地址翻譯成對應的物理地址,並決定如何將數據從發送方路由到接收方。
路由器屬於網絡層。此層的數據成爲數據包,需要關注的協議是IP協議。
1.4 傳輸層
傳輸層解決了主機間的數據傳輸,同時解決了傳輸質量的問題,該層需要關注的是TCP協議
和UDP協議。
1.5 會話層
作用是建立和管理應用程序之間的通訊。
1.6 表示層
解決信息的語法語義及它們的關聯,如加密解密、轉換翻譯、壓縮解壓縮。
1.7 應用層
規定應用發送方和接收方必須使用固定長度的消息頭,且有固定規則,需要關注的是HTTP
協議。
2 TCP/IP
OSI只是一個概念模型,TCP/IP可以看成OSI的“實現”
OSI七層模型 | TCP/IP模型 | 功能 | TCP/IP協議族 |
---|---|---|---|
應用層 | 應用層 | 文件傳輸,電子郵件、文件服務,虛擬終端 | TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet |
表示層 | 數據格式化,代碼轉換,數據加密 | 沒有協議 | |
會話層 | 解除或建立與別的接點的聯繫 | 沒有協議 | |
傳輸層 | 傳輸層 | 提供端對端的接口 | TCP,UDP |
網絡層 | 網絡層 | 爲數據包選擇路由 | IP,ICMP,RIP,OSPF,BGP,IGMP |
數據鏈路層 | 鏈路層 | 傳輸有地址的幀以及錯誤檢測功能 | SLIP,CSLIP,PPP,ARP,RARP,MTU |
物理層 | 以二進制數據形式在物理媒體上傳輸數據 | ISO2110,IEEE802,IEEE802.2 |
3 說說TCP的三次握手
3.1 傳輸控制協議TCP簡介
- 面向連接的、可靠的、基於字節流的傳輸層通信協議
- 將應用層的數據流分割爲報文段併發送給目標節點的TCP層
- 數據包都有序號,對方收到則發送ACK確認,未收到則重傳
- 使用校驗和來檢驗數據在傳輸過程中的是否有誤
3.2 TCP報文頭
- 源端口,目的端口:各佔2個字節
- 序列號:佔4個字節,因爲字節流中每個字節都按順序標號,這就是那個號
- 確認號:佔4個字節,是期望收到的下一個字節的序號
- Offset:指出TCP數據距離TCP報文的起始處有多遠
- Reserved:保留域,默認爲0
- TCP Flags:佔一個字節,詳情見3.3
- Window:代表滑動窗口的大小,用來告知發送端接收端的緩存大小,以此控制發送端的發送速率,從而達到流量控制。
- 校驗和:佔2個字節
- 緊急指針:僅當URG爲1時有效,指出報文的緊急數據的字節數
10.TCP可選項:定義其他的可選參數
3.3 TCP Flags
- URG:緊急指針標誌
- ACK:確認序號標誌(1表示有,0則忽略)
- PSH:push標誌
- RST:重置連接標誌
- SYN:同步序號,用於建立連接過程
- FIN:finish標誌,用於釋放連接
3.4 三次握手流程
注意:前兩次握手不可以攜帶數據,第三次握手可帶可不帶,若不帶,則不會消耗序號。
(1)第一次握手:建立連接時,客戶端發送SYN包(syn=j)到服務端,並今日SYN_SEND狀態,等待服務端確認;
(2)第二次握手:服務端接收到SYN包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RCVD狀態;
(3)第三次握手:客戶端接收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器今日ESTABLISHED狀態,完成三次握手
3.5 爲什麼需要三次握手才能建立連接
爲了初始化Sequence Number的初始值。
通信雙方要通知給對方自己初始化的Sequence Number,這個序列號要作爲以後數據通訊的序號,以保證傳輸層接受到的數據不會因網絡上的傳輸問題而亂序,即TCP會用這個序號來拼接數據,因此在服務器回發它的Sequence Number,即第二次握手之後,還需要發送確認報文給服務器,告知服務器客戶端已經收到你的Sequence Number了。
3.6 首次握手的隱患——SYN超時
問題起因分析
- Service收到Client的SYN,回覆SYN-ACK的時候未收到ACK
- Service不斷重試直至超時,Linux默認等待63秒才斷開連接(重試5次,每個等待時間翻倍,即等待1s,2s,4s,8s,16s,發送五次後,會在等待32秒才判定超時,總共63s)
針對SYN Flood的防護措施 - SYN隊列滿後,TCP會通過源地址端口、目的地址端口、時間戳打造一個特殊的Sequence NUmber,即tcp_syncookies參數(簡稱SYN Cookie),發回去。
- 若爲攻擊者,不會有相應;若爲正常連接,則會將這個SYN Cookie發回來,直接建立連接。
- 通過SYN Cookie,即便SYN隊列滿了,本請求不在隊列中,依然能偶簡歷連接
3.7 建立連接後,Clinet出現故障怎麼辦
保活機制
- 在一段時間內,即保活時間,稱爲keep alive time,連接處於非活動狀態,開啓保活機制的一方將向對方發送保活探測報文,如果未收到相應則重發
- 直到嘗試次數達到保活探測數,仍未收到相應則中斷連接
4 談談TCP的四次揮手
注1:這一過程又客戶端或服務端任意一端執行close來觸發,這裏假設客戶端主動觸發close。
注2:MSL代表最大報文生存時間
4.1 四次揮手流程
(1)第一次揮手:Client發送一個FIN,用來關閉Client到Server的數據傳送,Client進入FIN_WAIT_1狀態
(2)第二次揮手:Server收到FIN後,發送一個ACK給Cient,確認確認序號爲收到序號+1,Server進入CLOSE_WAIT狀態
(3)第三次揮手:Server發送一個FIN,用來關閉Server到Client的數據傳送,Server進入LAST_ACK狀態
(4)第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號爲收到序號+1,Server進入CLOSED狀態,完成四次揮手。
4.2 爲什麼會有TIME_WAIT狀態
原因:
- 確保有足夠的時間讓對方收到ACK包
- 避免新舊連接混淆
4.3 爲什麼需要四次揮手才能中斷連接
因爲全雙工(即:客戶端和服務端都可向對方發送數據),發送方和接收方都需要FIN報文和ACK報文
4.4 服務器大量出現CLOSE_WAIT狀態的原因
對方關閉socket連接,我方忙於讀或寫,沒有及時關閉連接
如何處理?
- 檢查代碼,特別是釋放資源的代碼
- 檢查配置,特別是處理請求的線程配置(比如線程池)
// 查看CLOSE_WAIT的數量
$ netstat -n | awk '/^tcp/{++S[$NF]}END{for(a in S) print a,S[a]}'
5 說說UDP
5.1 UDP報文結構
5.2 UDP特點
- 面向無連接
- 不維護連接狀態,支持同時向多個客戶端傳輸相同的消息
- 數據包報頭只有8個字節,額外開銷小
- 吞吐量只受限於數據生成速率、傳輸速率以及機器性能
- 盡最大努力交付,不保證可靠交付,不需要維持複雜的鏈接狀態表
- 面向報文,不對應用程序提交的報文信息進行拆分或者合併
6 TCP和UDP的區別
- 面向連接 vs 無連接
- 可靠性
- 有序性
- 速度
- 量級
7 TCP的滑動窗口
前置概念:
- RTT:發送一個數據包到收到對應的ACK,所花費的時間
- RTO:重傳時間間隔(不固定,由RTT計算而來)
TCP使用滑動窗口做流量控制與亂序重排,有兩個作用:
- 保證TCP的可靠性
- 保證TCP的流控制特性
TCP頭部中的Window字段用於接受方告知發送方自己緩存的剩餘空間,發送方根據接受方的處理能力發送數據,不會導致接受方處理不過來。
窗口數據的計算過程
首先明確左邊緩衝中的這些段的意思:
LastByteAcked之前:已發送且已確認的數據
LastByteAcked 到 LastByteSent:已發送但未確認的數據
LastByteSent 到 LastByteWritten:上層應用已寫完,但還未發送的數據
在明確右邊緩衝中的這些段的意思:
LastByteRead之前:上層應用已經讀完,也都發送回執的數據
LastByteRead 到 LastByteExpected:已經收到,但還未發送回執的數據
LastByteRcvd:接收到的最後一個字節的位置
- 接收方還可以處理的量: AdvertisedWindow = MaxRcvBuffer - (LastByteRcvd - LastByteRead)
- 窗口內剩餘可發送數據的大小:EffectiveWindow = AdvertisedWindow - (LastByteSent - LastByteAcked)
TCP滑動窗口原理
TCP會話的發送方:
其中,類別2發送但未確認這段和類別3未發送但允許發送這段組成的這段連續空間成爲發送窗口。
注意:只有當左邊數據(上圖是指32到35數據都被確認了)被連續確認之後,滑動窗口才會向右移動,新包括進來的數據纔可以發送。
TCP會話的接收方 ![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200512204450104.png) 其中,類別3未接收但準備接收的這段窗口成爲**接收窗口**。只有當左邊都接受到數據後,纔會向右滑動。
8 HTTP協議
8.1 超文本傳輸協議HTTP的主要特點
- 支持客戶/服務器模式
- 簡單快速(僅需要請求方法和地址)
- 靈活(傳輸類型多)
- 無連接
- 無狀態(對事務處理沒有記憶)
HTTP1.1相較於HTTP1.0最大的特點是:增加了keep-alive
8.2 HTTP請求結構
注意:請求頭部與請求正文必有一個空行
8.3 HTTP響應結構
注意:響應頭部與相應正文必有一個空行
8.4 請求/相應步驟
- 客戶端連接到Web服務器
- 發送HTTP請求
- 服務器接受請求並返回HTTP響應
- 釋放連接TCP連接
- 客戶端瀏覽器解析HTML內容
9 在瀏覽器地址欄鍵入URL,按下回車之後經歷的流程
- DNS解析。服務器會依據URL逐層查詢DNS服務器緩存,解析URL域名所對應的IP地址。DNS緩存由近到遠依次是:瀏覽器緩存、系統緩存、路由器緩存、IPS服務器緩存、根域名服務器緩存、頂級域名服務器緩存。從哪個緩存找到對應的IP,就直接返回,不再繼續查詢
- TCP連接。即:三次握手
- 發送HTTP請求。
- 服務器處理請求並返回HTTP報文。
- 瀏覽器解析渲染頁面。
- 連接結束。即:四次揮手
注:第5步與第6步可同時發生,哪個在前沒有特別要求。
10 HTTP狀態碼
10.1 五種可能取值
- 1××:指示信息——表示請求已接收,繼續處理
- 2××:成功——表示請求已被成功接受、理解、接受
- 3××:重定向——要完成請求必須進行更進一步的操作
- 4××:客戶端錯誤——請求有語法錯誤或請求無法實現
- 5××:服務端有錯誤——服務器未能實現合法的請求
10.2 常見狀態碼
200 OK:正常返回信息
301:永久重定向
302:臨時重定向
400 Bad Request:客戶端請求有語法錯誤,不能被服務器所理解
401 Unauthorized:請求未經授權,這個狀態碼必須和WWW-Authenticate報頭域一起使用
403 Forbidden:服務器收到請求,但是拒絕提供服務
404 Not Found:請求資源不存在,比如輸錯了URL
500 Internal Server Error:服務器發生了不可預期的錯誤
503 Server Unavailable:服務器當前不能處理客戶端的請求,一段時間後可能恢復正常。
11 GET請求和POST請求的區別
從三個層面來解答:
- HTTP報文層面:GET將請求信息放在URL,POST放在報文體中(因此GET對數據長度具有長度限制、POST沒有限制)
- 數據庫層面:GET符合冪等性(即:對數據庫的一次操作和多次操作獲得數據是一致的)和安全性(即:沒有改變數據庫中的數據),POST不符合
- 其他層面:GET可以被緩存、被存儲(大部分請求被CDN處理了,大大減少服務器壓力),而POST不行
12 Cookie和Session
12.1 Cookie簡介
- 是由服務器發送給客戶端的特殊信息,以文本的形式存放在客戶端
- 客戶端再次請求的時候,會把Cookie回發
- 服務器接收到後,會解析Cookie生成與客戶端相對應的內容
12.2 Cookie的設置以及發送過程
12.3 Session簡介
- 服務器端的機制,在服務器上保存的信息(類似於散列形式)
- 解析客戶端請求並操作session id,按需保存狀態信息
先檢查請求裏是否包含一個session id,若有,則使用此id獲取信息;若沒有,則創建一個session,並創建一個對應的session id,並返回回去。
12.4 Session的實現方式
- 使用Cookie來實現(服務器爲每個客戶端分配一個JSESSIONID,客戶端發起請求時,會在Cookie頭中攜帶這個JSESSIONID)
- 使用URL回寫來實現(服務器在給客戶端發送的每個頁面都攜帶一個JSESSION,客戶端點擊任意一個連接都會將JSESSIONID帶回去)
注:Tomat同時使用這兩種方式,若支持Cookie則使用第一種,若不支持,則使用第二種。
12.5 Cookie和Session的區別
- Cookie數據存放在客戶的瀏覽器上,Session數據放在服務器
- Session相對於Cookie更安全
- 若考慮減輕服務器負擔,應當使用Cookie
13 HTTPS
13.1 HTTPS簡介
保護交換數據隱私、完整性以及身份認證的功能
13.2 SSL(Security Sockets Layer,安全套接層)
- 爲網絡通信提供安全及數據完整性的一種安全協議
- 是操作系統對外的API,SSL3.0後更名爲TLS
- 採用身份驗證和數據加密保證網絡通信的安全和數據的完整性
13.3 加密方式
- 對稱加密:加密和解密都使用同一個密鑰
- 非對稱加密:加密使用的密鑰和解密使用的密鑰是不相同的
- 哈希算法:將任意長度的信息轉換成固定長度的值,算法不可逆
- 數字簽名:證明某個消息或者文件是某人發出/認同的
13.4 HTTPS數據傳輸流程
- 瀏覽器將支持的加密算法信息發送給服務器
- 服務器選擇一套瀏覽器支持的加密算法,以證書的形式回發瀏覽器
- 瀏覽器驗證證書合法性,並結合證書公鑰加密信息發送給服務器
- 服務器使用私鑰解密信息,驗證哈希,加密響應消息回發瀏覽器
- 瀏覽器解密響應信息,並對消息進行驗證,之後進行加密交互數據
13.5 HTTP和HTTPS的區別
- HTTPS需要到CA申請證書,HTTP不需要
- HTTPS密文傳輸,HTTP明文傳輸
- 連接方式不同,HTTPS默認使用443端口,HTTP使用80端口
- HTTPS=HTTP+加密+認證+完整性保護,較HTTP安全
13.6 HTTPS真的很安全嗎
那倒未必
- 瀏覽器默認填充http://,請求需要進行跳轉,又被劫持的風險
- 可使用HSTS(HTTP Strict Transport Security)優化
14 Socket
14.1 Socket簡介
Socket是對TCP/IP協議的抽象,是操作系統對外開放的接口
14.2 Socket通信流程
14.3 Socket相關的面試題
編寫一個網絡應用程序,有客戶端與服務器端,客戶端向服務器發送一個字符串,服務器收到
該字符串後將其打印到命令行上,然後向客戶端返回該字符串的長度,最後,客戶端輸出服務器
端返回的該字符串的長度,分別用TCP和UDP兩種方式去實現
TCP:
LengthCalculator.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class LengthCalculator extends Thread{
// 以socket爲成員變量
private Socket socket;
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();
}
}
}
TCPServer.java
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception{
// 創建socket,並將socket綁定到6499端口
ServerSocket ss = new ServerSocket(6499);
// 死循環,使得socket一直等待並處理客戶端發送過來的請求
while (true) {
// 監聽65000端口,直到客戶端返回連接信息後才返回
Socket socket = ss.accept();
// 獲取客戶端的請求信息後,執行相關業務邏輯
new LengthCalculator(socket).start();
}
}
}
TCPClient.java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception{
// 創建socket,並指定連接是本機的端口號爲6499的服務器socket
Socket socket = new Socket("127.0.0.1", 6499);
// 獲取輸出流
OutputStream os = socket.getOutputStream();
// 獲取輸入流
InputStream is = socket.getInputStream();
// 將要傳遞給server的字符串參數轉換給byte數組,並將數組寫入到輸入流中
os.write("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();
}
}
先執行Server,後執行Client,結果如下:
UDP:
UDPServer.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
public static void main(String[] args) throws Exception{
// 服務器接受客戶端發送的數據報
DatagramSocket socket = new DatagramSocket(6599); // 監聽的端口號
byte[] buff = new byte[100]; // 存儲從客戶端接收到的內容
DatagramPacket packet = new DatagramPacket(buff, buff.length);
// 接受客戶端發送過來的內容,並將內容封裝進DatagramPacket對象中
socket.receive(packet);
byte[] data = packet.getData(); // 從DatagramPacket對象中獲取真正存儲的數據
// 將數據從二進制轉換成字符串形式
String content = new String(data, 0, packet.getLength());
System.out.println(content);
// 將要發送給客戶端的數據轉換成二進制
byte[] sendedContent = String.valueOf(content.length()).getBytes();
// 服務器給客戶端發送數據報
// 從DatagramPacket對象中獲取到數據的來源地址與端口號
DatagramPacket packetToClient = new DatagramPacket(sendedContent,
sendedContent.length, packet.getAddress(), packet.getPort());
socket.send(packetToClient);
}
}
UDPClient.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) throws Exception{
// 客戶端發送數據報給服務端
DatagramSocket socket = new DatagramSocket();
// 要發送給服務端的數據
byte[] buf = "Hello UDP".getBytes();
// 將IP地址封裝成InetAddress對象
InetAddress address = InetAddress.getByName("127.0.0.1");
DatagramPacket packet = new DatagramPacket(buf, buf.length, address,
6599);
// 發送數據給服務端
socket.send(packet);
// 客戶端接受服務端發送過來的數據報
byte[] data = new byte[100];
// 創建DatagramPacket對象用來存儲服務端發送過來的數據
DatagramPacket reveivedPacket = new DatagramPacket(data, data.length);
// 將接受到的數據存儲到DatagramPacket對象中
socket.receive(reveivedPacket);
// 將服務端發送過來的數據取過來並打印到控制檯
String content = new String(reveivedPacket.getData(), 0,
reveivedPacket.getLength());
System.out.println(content);
}
}
先執行Server,後執行Client,結果如下: