網絡通信概念
網絡通信因為要處理復雜的物理信號,錯誤處理等,所以采用了分層設計。
為什么要采用分層設計?
1. 每層可以獨立開發,測試和替換;
2. 發生問題也可以快速定位到具體層次;
3. 協議標準化,不同廠商設備可通過標準協議實現互操作;
4. 降低復雜度,將通信功能拆解為獨立的功能模塊;
主流分層模型對比
模型 | 層次數 | 典型應用場景 | 特點 |
---|---|---|---|
OSI七層模型 | 7層 | 理論教學、復雜網絡設計 | 嚴格分層,功能細分(如會話層管理會話,表示層處理加密) |
TCP/IP四層模型 | 4層 | 實際網絡部署(如互聯網) | 合并OSI的會話層、表示層到應用層,更貼近實際實現 |
五層模型 | 5層 | 教材簡化教學 | 合并物理層和數據鏈路層為“網絡接口層”,保留OSI的核心邏輯 |
OSI七層模型
一、物理層
- 功能:傳輸原始比特流(0和1),定義物理介質的電氣、機械和過程規范(如電壓、電流、電纜類型、連接器形狀等)。
- 設備:中繼器、集線器、網線、光纖。
- 示例:通過網線傳輸電信號,或通過光纖傳輸光信號。
二、數據鏈路層
- 功能:
- 將比特流組裝成幀(Frame),添加幀頭和幀尾用于同步和錯誤檢測。
- 通過MAC地址實現節點間通信,管理物理尋址。
- 提供流量控制和差錯糾正(如CRC校驗)。
- 設備:交換機、網橋、網卡。
- 協議:Ethernet(以太網)、Wi-Fi(IEEE 802.11)、PPP(點對點協議)。
- 示例:交換機根據MAC地址將數據幀轉發到目標設備。
三、網絡層
- 功能:
- 通過IP地址實現邏輯尋址和路由選擇,確定數據包從源到目標的最佳路徑。
- 處理分組轉發和擁塞控制。
- 設備:路由器。
- 協議:IP(網際協議)、ICMP(互聯網控制報文協議)、ARP(地址解析協議)。
- 示例:路由器根據IP地址將數據包從一個網絡轉發到另一個網絡。
四、傳輸層
- 核心功能:提供端到端的可靠或不可靠傳輸,管理數據分段和重組。
- 關鍵協議:
- TCP(傳輸控制協議):
- 面向連接:傳輸前需建立三次握手(SYN→SYN-ACK→ACK),斷開時需四次揮手(FIN→ACK→FIN→ACK)。
- 可靠傳輸:通過確認機制、重傳和流量控制確保數據無差錯、不丟失、按序到達。
- 應用場景:文件傳輸(FTP)、網頁瀏覽(HTTP/HTTPS)、郵件(SMTP)。
- UDP(用戶數據報協議):
- 無連接:無需建立連接,直接發送數據包。
- 不可靠傳輸:不保證數據順序或到達,但開銷小、速度快。
- 應用場景:實時通信(視頻通話、在線游戲)、DNS查詢、直播流。
- TCP(傳輸控制協議):
- 端口號:傳輸層用16位端口號標識應用進程(如HTTP默認端口80)。
五、會話層
- 功能:
- 建立、管理和終止應用程序間的會話(連接)。
- 提供同步點(Checkpoint),確保數據傳輸的完整性。
- 協議:RPC(遠程過程調用)、SQL(數據庫查詢)。
- 示例:通過會話層保持用戶登錄狀態,直到主動退出。
六、表示層
- 功能:
- 數據格式轉換(如ASCII與Unicode編碼互換)。
- 數據加密/解密(如HTTPS中的TLS/SSL)、壓縮/解壓。
- 協議:JPEG(圖片壓縮)、MPEG(視頻壓縮)、SSL/TLS(安全傳輸)。
- 示例:瀏覽器將加密的HTTPS數據解密后顯示為網頁。
七、應用層
- 功能:直接為用戶應用程序提供網絡服務接口。
- 協議:
- HTTP/HTTPS:網頁傳輸。
- FTP/SFTP:文件上傳/下載。
- SMTP/POP3/IMAP:郵件收發。
- DNS:域名解析(將域名轉為IP地址)。
- Telnet/SSH:遠程登錄。
- 示例:瀏覽器通過HTTP協議訪問網站,郵件客戶端通過SMTP協議發送郵件。
TCP/IP 四層模型
將OSI七層模型中的物理層、數據鏈路層合并為網絡接口層,并將會話層、表示層、應用層合并為應用層,最終形成四層結構(網絡接口層、網絡層、傳輸層、應用層)。
為什么要進行這種合并?
1. 物理層、數據鏈路層功能高度耦合
2. 合并會話層、表示層、應用層大大簡化了開發流程,會話管理和數據壓縮功能可直接在應用層升級,無需修改底層協議。
RPC屬于哪一層?
根據上面的介紹和前文RPC框架基本概念可以得出一個結論:在OSI七層協議中,RPC跨越了傳輸層,表示層,會話層和應用層;而在TCP/IP協議中,RPC跨越了傳輸層和應用層。
Socket套接字
socket是網絡編程的核心抽象,屏蔽了底層網絡協議的復雜性,為應用程序提供了統一的接口來發送和接收數據。屬于應用層和傳輸層之間的編程接口。
各大操作系統的內核實現了Socket的底層邏輯,然后程序員就可以通過這些接口(API)來使用網絡或進程間的通信功能,無需直接操作底層硬件或者協議細節,這也就是我們說的網絡編程。
IO基本概念
什么是 IO?
指的是計算機內存和外部設備之前拷貝數據的過程。
網絡 I/O
指內存與遠程主機(通過網卡和網絡介質)之間的數據傳輸
傳統I/O | 網絡I/O | 共性 | |
---|---|---|---|
數據流向 | 內存 ? 本地存儲設備 | 內存 ? 遠程主機(通過網絡) | 均涉及內存與外部實體的數據交換 |
核心操作 | 讀寫磁盤塊 | 發送/接收數據包 | 均通過系統調用觸發數據流動 |
優化目標 | 減少磁盤尋道、拷貝次數 | 降低延遲、提高吞吐量 | 均圍繞“高效數據交換”展開 |
術語來源 | 輸入/輸出(Input/Output) | 繼承自傳統I/O的抽象命名 | 統一描述所有“內存與外部交互”的場景 |
關鍵步驟
網絡數據到達網卡后要拷貝到內核空間(操作系統內核運行的內存區域,擁有最高權限,可直接訪問硬件資源(如CPU、內存、設備等))
網絡數據再從內核空間拷貝到用戶空間(用戶程序運行的內存區域,權限較低,無法直接訪問硬件或內核數據)
因此,根據讀寫數據時候的不同表現形式,就會衍生出不同的IO模型。
在Linux操作系統中,根據數據從內核空間到用戶空間的拷貝方式及進程的阻塞狀態,主要存在以下五種I/O模型:
- 同步阻塞IO
- 同步非阻塞IO
- IO多路復用 (當前最流行)
- 信號驅動IO
- 異步IO
關于基本名詞的解釋:
? ? ? ? 同步:應用程序想要獲取數據需要主動調用系統函數獲取
? ? ? ? 阻塞:應用程序調用系統函數獲取數據的時候還沒有數據要等待數據的到來
同步阻塞IO
服務端代碼
public class BioBlock {public static void main(String[] args) {ServerSocket serverSocket = null;BufferedReader in = null;BufferedWriter out = null;try {// 創建服務端Socket,綁定接口 開啟監聽serverSocket = new ServerSocket(9999);// socket手動獲取一個客戶端連接final Socket socket = serverSocket.accept();in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));String line = in.readLine();if (line == null){return ;}out.write("hello client" + socket.getInetAddress() + " i am server");out.flush();} catch (IOException e) {throw new RuntimeException(e);}}
}
客戶端代碼
public class BioClient {public static void main(String[] args) {Socket socket = null;BufferedReader in = null;BufferedWriter out = null;try {// 客戶端Socket 通過ip+端口找到服務器socket = new Socket("127.0.0.1",9999);in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));out.write("hello server,i am client! \n");out.flush();String line = in.readLine();System.out.println("msg from server:" + line);} catch (UnknownHostException e) {throw new RuntimeException(e);} catch (IOException e) {throw new RuntimeException(e);}}
}
可以看到,服務端每次接收到一條信息,都需要阻塞等信息讀取才能接收其他客戶端發來的信息,很不方便,效率很低。
改進方法為每次服務端接收到一個請求,就開辟一個線程來接收客戶端發來的信息。
改進代碼如下:
服務端代碼
public class NewBioBlock {public static void main(String[] args) {ServerSocket serverSocket = null;BufferedReader in = null;BufferedWriter out = null;try {// 創建服務端Socket,綁定接口 開啟監聽serverSocket = new ServerSocket(9999);while(true){Socket socket = serverSocket.accept();new Thread(new BioClientThread(socket)).start();}} catch (IOException e) {throw new RuntimeException(e);}}
}
客戶端
public class BioClientThread implements Runnable{Socket socket = null;BufferedReader in = null;BufferedWriter out = null;BioClientThread(Socket socket){this.socket = socket;}@Overridepublic void run() {try {in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));while(true){String line = in.readLine();if (line == null){return ;}out.write("hello client" + socket.getInetAddress() + " i am server");out.flush();}} catch (IOException e) {throw new RuntimeException(e);}}
}
可以發現還是有一個問題,就是客戶端的并發數與后端的線程數是1:1,如果連接量增大,服務器壓力就會過大,甚至會發生堆棧溢出的情況;
就算采用了線程池也無法解決改問題,因為如果連接沒有斷開改線程會一直占用,不能給其他客戶端建立連接。
這種情況只適合點對點的情況,對并發數要求不高。
同步非阻塞IO
相比于同步阻塞IO,同步非阻塞IO在應用程序調用read等系統函數獲取數據但是還沒有數據的時候,直接返回,而不是等待數據的到來。
注意同步非阻塞在沒有數據到來的情況下直接返回,那么數據到來后又怎么辦呢,所以同步非阻塞IO需要在應用程序層做一個read調用輪詢操作。
public class NioServer { // 定義服務器類public static void main(String[] args) { // 程序入口try {// 1. 創建服務端通道(相當于開了個門面)ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 綁定9999端口(門面掛上9999號招牌)serverSocketChannel.bind(new InetSocketAddress(9999));// 3. 設置非阻塞模式(店員不會傻等客人,而是同時看多個門)serverSocketChannel.configureBlocking(false);// 4. 創建多路復用器(相當于監控攝像頭)Selector selector = Selector.open();// 5. 注冊連接事件(監控攝像頭對準9999門面,發現有人來就亮燈)serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 6. 事件輪詢(監控室24小時值班)while (true){// 7. 等待事件發生(監控畫面出現動靜)selector.select();// 8. 獲取所有觸發事件的鑰匙(拿到所有亮燈的監控畫面)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()){// 9. 取出第一把鑰匙SelectionKey selectionKey = iterator.next();// 10. 處理完要銷毀證據(避免重復處理)iterator.remove();// 11. 根據鑰匙類型處理具體事件processSelectedKey(selectionKey,selector);}}} catch (IOException e) {// 12. 異常處理(出問題直接撂挑子)throw new RuntimeException(e);}}private static void processSelectedKey(SelectionKey selectionKey, Selector selector) throws IOException {// 13. 檢查鑰匙是否有效(門是否被拆了)if (selectionKey.isValid()) {// 14. 判斷是否是新連接事件(有人推門進來)if (selectionKey.isAcceptable()) {// 15. 獲取門面對應的通道ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();// 16. 接受新連接(把客人迎進門)SocketChannel socketChannel = serverSocketChannel.accept();// 17. 設置非阻塞(客人不會堵住門口)socketChannel.configureBlocking(false);// 18. 注冊讀事件(給客人遞上意見簿)socketChannel.register(selector, SelectionKey.OP_READ);return; // 處理完連接事件直接返回}// 19. 判斷是否有數據可讀(客人寫意見了)if (selectionKey.isReadable()) {// 20. 獲取客戶端通道SocketChannel socketChannel = (SocketChannel) selectionKey.channel();// 21. 準備意見簿(1024字節的緩沖區)ByteBuffer readBuffer = ByteBuffer.allocate(1024);// 22. 讀取意見(可能客人寫了0-1024字節)int read = socketChannel.read(readBuffer);// 23. 確實有內容才處理if (read > 0 ) {// 24. 翻到意見頁(切換緩沖區為讀模式)readBuffer.flip();// 25. 撕下意見頁(復制數據到字節數組)byte[] bytes = new byte[readBuffer.remaining()];readBuffer.get(bytes);// 26. 破譯客人筆跡(轉為字符串)String msg = new String(bytes, Charset.defaultCharset());System.out.println("服務端收到來自客戶端的數據:" + msg);// 27. 準備回信(256字節的回復緩沖區)ByteBuffer sendBuffer = ByteBuffer.allocate(256);sendBuffer.put("hello nio client ,i am nio server \n".getBytes(StandardCharsets.UTF_8));// 28. 翻到回信頁(切換為讀模式)sendBuffer.flip();// 29. 塞給客人(發送回復)socketChannel.write(sendBuffer);}return;}}}
}
ByteBuffer 的直接緩沖區(Direct Buffer)是在操作系統管理的物理內存中分配空間,而不是在JVM堆內存開辟空間,這樣可以通過減少用戶空間與內核空間之間的數據拷貝次數,顯著提升 I/O 操作的性能。
需要應用程序做輪詢調用,有系統開銷并且不會釋放線程資源。
IO多路復用
同步阻塞IO和同步非阻塞IO有一個問題,就是不知道什么時候數據會到來,需要不停的檢測。而多路復用IO能提前知道Socket是否有數據到來,有再調用系統函數read。
簡而言之IO多路復用其實在思想上和同步非阻塞IO有相似之處,但是IO多路復用將事件檢測邏輯下沉到內核,減少用戶態與內核態的上下文切換,性能自然更高。
IO多路復用模型下提供了幾種不同的系統調用來檢測socket是否有數據到來:
- select
- poll
- epoll
- kqueue
select
平臺移植性很好
通過輪詢方式檢查一組文件描述符(fd)的狀態(可讀、可寫、異常)。
用戶態需傳遞三個文件描述符集合(readfds
、writefds
、exceptfds
),內核遍歷集合并返回就緒的描述符數量。
缺點:
1. 可以監聽的fd數量有限,默認是1024個。
2. 需要維護一個存放大量fd的數據結構,調用select時這些數據從用戶空間拷貝到內核空間開銷大,但是對比同步非阻塞IO就快很多了,因為只需要拷貝fd_set集合一次到內核態,再在內核態遍歷fd_set集合。
3. select調用返回后需要線性掃描fd_set來獲取就緒的fd,fd數量增多遍歷速度就慢。
poll
poll調用 和 select 調用差不多,poll是對select的改進,理論上可監聽的fd沒有select那樣的數量限制(動態數組),而且針對于fd_set遍歷事件復雜度更加低,更高效。
epoll
在linux平臺上面十分成熟,但移植到其他平臺有障礙
可監聽的fd數量理論上沒有限制,支持fd數量上限是最大可以打開文件的數量,可以查看/proc/sys/fs/file-max
進一步減少了從內核態到用戶態的拷貝次數,也不需要維護一個fd集合,不需要在用戶空間和內核空間來回拷貝全量fd,應用不需要線性遍歷所有fd來獲取就緒的fd,epoll_wait返回的就是已就緒的,復雜度大大降低,性能提高。
采用紅黑樹管理所有注冊的文件描述符,支持高效插入、刪除和查找。通過?epoll_ctl
?函數一次性注冊文件描述符,避免重復拷貝,僅在文件描述符狀態變化時更新內核數據結構。
異步IO
同步IO指的是socket緩沖區有數據后應用主動調用read等系統函數來完成從內核緩沖區到用戶空間的拷貝,并且拷貝過程中應用線程是阻塞住的。
上面介紹的三種模型都屬于同步IO
異步IO(AIO)指的是整個過程均由內核完成,即內核等待數據到來,內核讀取數據到內核緩沖區,然后將數據拷貝到用戶空間,完成后通知用戶進程,整個IO操作期間都不會阻塞用戶進程。
windows下實現成熟,但很少作為百萬級別以上高并發服務器操作系統來使用,因為其不如nio穩定。linux系統下并不完善,主要還是采用io多路復用模型為主。