計算機網絡
基礎知識
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);
}
}