目錄
一、背景
二、交互流程
2.1 數據流動
2.2 對象之間的關系
三、TCP
3.1 為什么需要三次握手
3.2?三次握手流程
3.3 三次握手后的產物
3.4 TCB
四、Socket
4.1?Java Socket和C++ Socket
4.2 Socket的本質
4.3 Socket和TCB的關系
4.4 通過文件描述符調用Socket的流程
五、Epoll
5.1 Epoll 結構
5.2 Epoll簡要工作流程
5.3 Epoll代碼
5.4?epoll_ctl過程
5.5 epoll_wait過程
5.6?水平觸發(LT)與邊緣觸發(ET)
5.7 與Java NIO關系
一、背景
網絡傳輸無處不在,正確理解網絡傳輸的步驟有助于我們寫出高性能的程序,也有助于我們解決程序中出現的問題。
二、交互流程
下面的不理解可以跳過?二、交互流程
2.1 數據流動
網卡 → DMA緩沖區 → 協議棧處理(IP/TCP) → TCB接收緩沖區 → recv() → 用戶空間緩沖區
應用程序 → send() → 用戶空間緩沖區 → 內核發送緩沖區 → 協議棧處理 → 網卡隊列 → 網絡
2.2 對象之間的關系
epoll
→fd
?→?struct file
?→?struct socket
?→?struct sock
(TCB 的核心數據結構)
三、TCP
老生常談的東西了,基本就是三次握手,但這次我會從操作系統角度,談談還干了什么,還包括三次握手的生成對象TCB
3.1 為什么需要三次握手
-
第一次握手(
SYN
):服務端確認客戶端的發送能力正常。 -
第二次握手(
SYN-ACK
):客戶端確認服務端的接收和發送能力正常。 -
第三次握手(
ACK
):服務端確認客戶端的接收能力正常。 -
只有三次握手后,雙方才能確保彼此能正常收發數據。
TCP三次握手發生在網絡協議的傳輸層
3.2?三次握手流程
-
客戶端發起連接
-
用戶調用
connect()
,內核發送SYN報文(設置初始序列號ISN
)。 -
創建TCB(傳輸控制塊):內核為連接分配資源(如
struct tcp_sock
),初始化序列號(ISN)、窗口大小等參數,TCB狀態變為SYN_SENT
-
-
服務端響應
-
DMA寫入內存:網卡通過DMA直接將報文數據(包括TCP頭、IP頭、以太網幀等)寫入內核預分配的接收緩沖區(如
sk_buff
結構)。 -
觸發軟中斷:隨后網卡觸發軟中斷(如Linux中的
NET_RX_SOFTIRQ
),通知內核有新的數據包需要處理。 -
創建半連接:
內核協議棧解析SYN包,創建傳輸控制塊(TCB),初始化連接狀態(如序列號、窗口大小),并將連接狀態設為SYN_RCVD
(半開連接)。 -
加入半連接隊列(SYN Queue):
該連接(TCB)被存入半連接隊列,等待客戶端確認。 -
構造SYN-ACK包:
內核生成SYN-ACK響應(設置SYN和ACK標志,分配服務器初始序列號,確認號為客戶端的序列號+1)。 -
DMA發送數據:
報文通過內核協議棧封裝(TCP頭→IP頭→MAC頭),存入網卡發送緩沖區,網卡通過DMA讀取并發送。、 -
啟動重傳定時器:
為防止丟包,內核啟動定時器(默認約1秒),若未收到客戶端的ACK,將重傳SYN-ACK
-
-
最終確認
-
客戶端收到SYN-ACK后,狀態變為
ESTABLISHED
,發送ACK。 -
服務端收到ACK后,做3-6步驟的操作
- DMA與軟中斷:客戶端的ACK包由網卡通過DMA寫入內存,再次觸發
NET_RX_SOFTIRQ
軟中斷。 - 驗證ACK:內核檢查ACK的合法性(確認號是否為服務器序列號+1)。
- 連接狀態遷移:若驗證通過,連接狀態轉為
ESTABLISHED
,并從半連接隊列移至全連接隊列(Accept Queue)。 - 通知應用層:應用通過
accept()
系統調用從全連接隊列中獲取新連接,開始數據傳輸。
-
3.3 三次握手后的產物
主要是TCB和全連接隊列
全連接隊列:存放已建立連接但未被accept()
取出的TCB
3.4 TCB
1. 存儲連接狀態信息
-
連接狀態:記錄 TCP 狀態機的當前階段(如?
ESTABLISHED
、TIME_WAIT
、SYN_RECEIVED
?等)。 -
端點信息:保存本地和遠端的 IP 地址及端口號,唯一標識一個 TCP 連接。
-
序列號和確認號:維護發送和接收數據的序列號(
SEQ
)和確認號(ACK
),保證數據有序性和可靠性。
2. 完成數據傳輸
-
每個 TCP 連接由?四元組?唯一標識:
<本地 IP, 本地端口, 遠端 IP, 遠端端口>
只要四元組中任意一個元素不同,內核就會視為?不同的 TCP 連接,并為其分配獨立的 TCB(傳輸控制塊)。當應用程序通過socket fd執行
read()/write()
時,內核會通過關聯的TCB完成實際的數據傳輸
3. 緩沖區和數據管理
-
發送緩沖區:暫存應用層待發送的數據,直到收到對方的確認。
-
接收緩沖區:存儲已接收但尚未被應用層讀取的數據。
4. 流量控制與窗口管理
-
滑動窗口:記錄接收方的可用緩沖區大小(窗口大小),控制發送速率以避免接收方溢出。
-
發送和接收窗口:跟蹤當前允許發送的數據范圍和已確認的數據范圍。
5. 連接生命周期管理
-
三次握手:跟蹤?
SYN
、SYN-ACK
、ACK
?的交換過程,完成連接建立。 -
四次揮手:管理?
FIN
?包的交換,確保連接正常關閉或終止。
????
四、Socket
4.1?Java Socket和C++ Socket
Java Socket 和 C++ Socket 在 Linux 上的本質是相同的,它們的底層實現均基于?Linux 內核提供的同一套 Socket 接口。無論是 Java 的?java.net.Socket
?還是 C++ 的?sys/socket.h
,最終都會通過系統調用(如?socket()
,?bind()
,?connect()
?等)與內核交互。區別僅在于語言層面的封裝和 API 設計。
既然是一樣的,我們后面主要是在操作系統層級進行分析。
4.2 Socket的本質
Socket是文件描述符的一種類型。文件描述符可以表示多種資源(文件、管道、Socket等),Socket是其中用于網絡通信的一種。
通過文件描述符操作Socket:Socket的讀寫、關閉等操作均可通過其關聯的文件描述符完成:
使用通用I/O函數:如?read()
、write()
、close()
。
使用Socket專用函數:如?send()
、recv()
、bind()
、connect()
?等,這些函數需要文件描述符作為參數。
4.3 Socket和TCB的關系
一一對應關系
-
每個 TCP Socket 對應一個 TCB:
-
當應用調用?
socket()
?創建一個 TCP Socket 后,內核會為這個 Socket 分配一個 TCB。 -
TCB 的生命周期與 Socket 綁定:Socket 被創建時 TCB 初始化,Socket 關閉時 TCB 釋放。
-
-
Socket 是 TCB 的“用戶態句柄”:
-
應用通過 Socket 文件描述符操作連接(如發送數據、接收數據、關閉連接),內核根據 Socket 找到對應的 TCB,修改其狀態或觸發協議行為(如重傳、流量控制)。
-
-
發送數據:
-
應用通過?
send()
?寫入 Socket 的數據會暫存到 TCB 的發送緩沖區,TCP 協議根據 TCB 中的窗口和擁塞控制參數決定何時發送。
-
-
接收數據:
-
內核將收到的數據存入 TCB 的接收緩沖區,應用通過?
recv()
?從 Socket 讀取時,數據從接收緩沖區復制到用戶空間。
-
每個 Socket 文件描述符(fd
)在內核中關聯到一個?struct socket
?結構,該結構指向對應的?struct sock
(即 TCB 的核心數據結構)。
關系鏈:
fd
?→?struct file
?→?struct socket
?→?struct sock
(含接收緩沖區)。
4.4 通過文件描述符調用Socket的流程
對象級調用流程:
用戶調用 read(sockfd, buf, len)↓ 通過 sockfd 找到進程文件描述符表中的 struct file↓ struct file 的 f_op->read() 調用 Socket 的具體實現(如 sock_read())↓ 內核通過 struct file 的 private_data 找到 struct socket↓ 最終操作 struct sock 的接收緩沖區(sk_receive_queue)讀取數據
接收流程:
-
數據到達內核:
數據包經過網卡接收、協議棧解析(IP/TCP 層處理)后,最終存入對應 TCP 連接的?TCB 接收緩沖區。該緩沖區位于內核空間,由內核管理。 -
應用程序調用?
recv()
:
當應用程序調用?recv(fd, buf, len, flags)
?時:-
fd
(文件描述符):關聯到特定的 Socket,而該 Socket 綁定到唯一的 TCB。 -
內核操作:
內核從該 TCB 的接收緩沖區中,復制數據到用戶提供的緩沖區?buf
(位于用戶空間)。 -
數據移除:
被成功復制的數據會從 TCB 的接收緩沖區中移除,釋放空間,接收窗口(rwnd
)隨之擴大。
-
-
返回結果:
-
若接收緩沖區中有數據,
recv()
?立即返回實際讀取的字節數。 -
若接收緩沖區為空:
-
阻塞模式:進程休眠,直到新數據到達或連接關閉。
-
非阻塞模式:立即返回錯誤碼(如?
EAGAIN
?或?EWOULDBLOCK
)。
-
-
Q:TCB接收緩沖區的作用是什么?
(1)數據暫存與排序
-
TCB 的接收緩沖區存儲已通過 TCP 協議驗證的、按序排列的數據。例如:
-
若數據包亂序到達,內核會在緩沖區中等待缺失的序列號填補后,再通知應用層讀取。
-
若數據重復(如重傳包),內核直接丟棄冗余數據。
-
(2)流量控制的基礎
-
接收緩沖區的剩余空間決定了 TCP 的?接收窗口(
rwnd
)。此窗口通過 ACK 報文通告給發送方,控制其發送速率。 -
若緩沖區滿,
rwnd=0
,發送方暫停發送,避免數據被丟棄。
(3)內核與用戶空間的橋梁
-
數據從內核的 TCB 接收緩沖區到用戶空間的?
buf
,必須通過?拷貝(如?copy_to_user()
)。
(注:零拷貝技術如?splice()
?或?sendfile()
?可繞過此步驟,但常規?recv()
?需要拷貝。)
發送流程:
1. 應用程序提交數據
-
send()
/write()
?系統調用:
應用程序將數據寫入用戶空間緩沖區,調用?send()
?觸發系統調用。 -
數據拷貝到內核:
數據從用戶空間拷貝到 TCB 的發送緩沖區(內核空間)。
2. 發送緩沖區管理
-
分片與封裝:
內核根據 MSS 將數據分片,添加 TCP/IP 頭部,生成 TCP 段。 -
發送窗口約束:
僅允許發送窗口內的數據(已發送未確認數據 + 可發送未發送數據 ≤ 發送窗口大小)。
3. 協議棧處理
-
滑動窗口與序列號:
每個 TCP 段攜帶序列號(seq
),接收方據此確認數據順序。 -
重傳機制:
已發送但未確認的 TCP 段保留在發送緩沖區的重傳隊列中,超時(RTO)或收到重復 ACK 時觸發重傳。
4. 網絡層與網卡發送
-
IP 層處理:
添加 IP 頭部,路由選擇,分片(若超過 MTU)。 -
網卡隊列:
TCP 段交給網卡驅動,存入發送隊列(如?tx_ring
),通過 DMA 發送到網絡。
五、Epoll
經過上面的介紹,我們對文件描述符、Socket、TCP有相應的了解,也明白了文件描述符是如何操作網絡連接的。
那假如有大量的網絡連接時,我們該如何管理呢?
目前主流的方法是I/O 多路復用,即單線程/少量線程監聽多個socket事件
Epoll是Linux下的一種I/O多路復用機制,用于高效地處理大量文件描述符
5.1 Epoll 結構
epoll
?的實現依賴于兩個核心數據結構:紅黑樹(Red-Black Tree)?和?就緒隊列(Ready List)。它們共同協作,使得?epoll
?能夠高效地管理海量文件描述符(FD)的 I/O 事件。
紅黑樹是?epoll
?實例中用于?存儲所有被監控的文件描述符(FD)?的數據結構。每個通過?epoll_ctl
?添加的 FD(如套接字、管道等)都會在紅黑樹中注冊為一個節點。
設計原因
-
高效動態操作:紅黑樹是一種自平衡二叉搜索樹,插入、刪除、查找的時間復雜度均為?
O(log N)
,適合頻繁增刪 FD 的場景(例如 Web 服務器處理大量短連接)。 -
快速定位 FD:當某個 FD 發生事件(如可讀、可寫)時,內核需要快速找到該 FD 的監控信息(例如用戶關注的事件類型),紅黑樹的特性確保了這一過程的效率。
-
避免重復注冊:紅黑樹的唯一性保證同一個 FD 不會被重復添加,避免資源浪費。
就緒隊列是?epoll
?實例中用于?臨時存儲已觸發事件的 FD?的鏈表結構。當某個被監控的 FD 發生事件(例如套接字收到數據),內核會將該 FD 添加到就緒隊列中。
設計原因
-
快速事件通知:用戶調用?
epoll_wait
?時,內核無需遍歷所有被監控的 FD,而是直接檢查就緒隊列,時間復雜度接近?O(1)
。 -
事件去重與合并:如果同一 FD 的多個事件連續觸發(如多次可讀),就緒隊列會合并這些事件,避免重復通知。
-
支持邊緣觸發(Edge-Triggered, ET)模式:在 ET 模式下,事件僅在狀態變化時觸發一次,就緒隊列確保事件不會被遺漏。
工作流程
-
事件觸發:當某個 FD 發生事件(如數據到達),內核調用與該 FD 關聯的回調函數。
-
加入就緒隊列:回調函數將 FD 插入就緒隊列,并標記觸發的事件類型。
-
用戶獲取事件:用戶調用?
epoll_wait
?時,內核將就緒隊列中的事件拷貝到用戶空間,并清空隊列(取決于觸發模式)。
5.2 Epoll簡要工作流程
-
創建epoll實例
-
使用?
epoll_create1()
?創建epoll文件描述符。
-
-
創建并配置監聽Socket
-
創建TCP Socket,設置為非阻塞模式,綁定地址并開始監聽。
-
-
注冊監聽Socket到epoll
-
通過?
epoll_ctl()
?將監聽Socket加入epoll監控,關注?EPOLLIN
?事件(新連接事件)。
-
-
事件循環
-
使用?
epoll_wait()
?阻塞等待事件發生。 -
遍歷就緒事件列表,處理不同類型的事件:
-
新連接:接受連接,將新Socket加入epoll監控。
-
數據可讀:讀取數據并處理,必要時注冊?
EPOLLOUT
?事件準備寫入。 -
數據可寫:發送數據,完成后取消?
EPOLLOUT
?監控。 -
錯誤/關閉:移除并關閉Socket。
-
-
-
清理資源
-
關閉所有Socket和epoll實例。
-
5.3 Epoll代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>#define MAX_EVENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024// 設置文件描述符為非阻塞模式
void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {int listen_fd, epoll_fd, nfds;struct epoll_event ev, events[MAX_EVENTS];struct sockaddr_in addr;// 1. 創建監聽Socketlisten_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 綁定并監聽memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_addr.s_addr = htonl(INADDR_ANY);addr.sin_port = htons(PORT);if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) {perror("bind");close(listen_fd);exit(EXIT_FAILURE);}if (listen(listen_fd, SOMAXCONN)) {perror("listen");close(listen_fd);exit(EXIT_FAILURE);}set_nonblocking(listen_fd); // 非阻塞模式// 2. 創建epoll實例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");close(listen_fd);exit(EXIT_FAILURE);}// 3. 注冊監聽Socket到epollev.events = EPOLLIN; // 關注可讀事件ev.data.fd = listen_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev)) {perror("epoll_ctl: listen_fd");close(listen_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 4. 事件循環while (1) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");break;}for (int i = 0; i < nfds; i++) {int fd = events[i].data.fd;// 處理新連接if (fd == listen_fd) {struct sockaddr_in client_addr;socklen_t addrlen = sizeof(client_addr);int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addrlen);if (client_fd == -1) {perror("accept");continue;}set_nonblocking(client_fd); // 新Socket設為非阻塞ev.events = EPOLLIN | EPOLLET; // 邊緣觸發模式(可選)ev.data.fd = client_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {perror("epoll_ctl: client_fd");close(client_fd);}printf("New connection: fd %d\n", client_fd);// 處理數據可讀} else if (events[i].events & EPOLLIN) {char buffer[BUFFER_SIZE];ssize_t nread = recv(fd, buffer, BUFFER_SIZE, 0);if (nread > 0) {printf("Received from fd %d: %.*s\n", fd, (int)nread, buffer);// 回顯數據(示例)send(fd, buffer, nread, 0);} else if (nread == 0 || (nread == -1 && errno != EAGAIN)) {// 關閉連接printf("Closing fd %d\n", fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);close(fd);}}// 處理其他事件(如EPOLLOUT、EPOLLERR等)}}// 清理資源close(listen_fd);close(epoll_fd);return 0;
}
5.4?epoll_ctl過程
當通過?epoll_ctl(EPOLL_CTL_ADD)
?將一個fd(如Socket)添加到epoll實例時,內核會執行以下操作:
(1) 注冊回調函數到fd的等待隊列
-
為fd創建epoll條目(
epitem
):包含fd的信息和關注的事件(如EPOLLIN
)。 -
將回調函數?
ep_poll_callback
?注冊到fd的等待隊列:-
調用fd的?
poll
?方法(如Socket的?sock_poll
)。 -
poll
?方法將?ep_poll_callback
?添加到fd的等待隊列中。
-
(2) 事件觸發時的回調流程
-
數據到達觸發硬件中斷:網卡接收數據后,內核協議棧處理數據并標記Socket為可讀。
-
喚醒等待隊列:
-
內核調用Socket等待隊列中的回調函數?
ep_poll_callback
。 -
ep_poll_callback
?將對應的fd(封裝為?epitem
)加入epoll的就緒隊列(rdllist
)。
-
-
通知用戶程序:
-
如果用戶程序阻塞在?
epoll_wait
,內核喚醒該線程,使其從?epoll_wait
?返回并處理就緒事件。
-
5.5 epoll_wait過程
epoll_wait
?是?epoll
?機制的核心函數,它負責?等待并獲取已就緒的事件。
步驟 1:進入內核態
-
用戶程序調用?
epoll_wait
?時,會從用戶態切換到內核態。 -
內核訪問?
epoll
?實例的數據結構(包括?紅黑樹?和?就緒隊列)。
步驟 2:檢查就緒隊列
-
epoll
?實例維護一個?就緒隊列(ready list),其中保存所有已觸發事件的fd。 -
如果就緒隊列非空,內核直接從中取出事件,填充到用戶空間的?
events
?數組。 -
如果隊列為空,且?
timeout=-1
,線程阻塞在此處,直到有新事件到來或信號中斷。
步驟 3:監控fd狀態(若就緒隊列為空)
-
若就緒隊列為空,內核通過?回調機制?監控所有注冊的fd:
-
當某個fd發生事件(如socket接收數據),內核會將該fd添加到就緒隊列。
-
這一過程由內核的?事件驅動機制?實現,無需輪詢所有fd,效率極高。
-
步驟 4:返回就緒事件
-
將就緒隊列中的事件復制到用戶空間的?
events
?數組。 -
返回就緒事件的數量?
nfds
,用戶程序通過遍歷?events[0..nfds-1]
?處理事件。 -
如果此時有線程阻塞在?
epoll_wait
,內核會喚醒該線程
5.6?水平觸發(LT)與邊緣觸發(ET)
行為 | 水平觸發(LT) | 邊緣觸發(ET) |
---|---|---|
觸發時機 | 只要緩沖區有數據/可寫,持續觸發 | 僅在緩沖區狀態變化時觸發一次(如新數據到達) |
數據未讀盡的后果 | 下次?epoll_wait ?繼續報告事件 | 不再觸發事件,可能導致數據滯留 |
編程復雜度 | 較低(無需一次性處理所有數據) | 較高(需循環讀寫至?EAGAIN ) |
適用場景 | 簡單場景、小數據量 | 高性能場景、需精細化控制 |
5.7 與Java NIO關系
Java NIO 在不同的操作系統和 JDK 版本中確實會使用不同的底層實現,其中在?Linux 系統上,Java NIO 的?Selector
(多路復用機制)默認是基于?epoll
?實現的