文章目錄
- 一、進程中如何通信
- 1.1 管道
- 1.1.1 核心特性
- 1.1.2 缺點
- 1.1.3 匿名管道與命名管道的對比
- 1.2 信號
- 1.2.1 核心特性
- 1.2.2 缺點
- 1.2.3 信號分類對比
- 1.3 消息隊列
- 1.3.1 核心特性
- 1.3.2 缺點
- 1.4 共享內存
- 1.4.1 核心特性
- 1.4.2 缺點
- 1.5 信號量
- 1.5.1 核心特性
- 1.5.2 缺點
- 二、Socket
- 2.1 Socket原理
- 2.1.1 什么是Socket
- 2.1.2 網絡進程如何通信
- 2.1.3 Sokcet如何通信
- 2.2 TCP/IP協議
- 2.2.1 概念
- 2.2.2 TCP數據報結構
- 2.3 連接建立(三次握手)
- 2.3.1 建立過程
- 2.3.2 關鍵問題
- 為什么是三次握手,而不是兩次四次?
- 2.4 斷開連接(四次揮手)
- 2.4.1 斷連過程
- 2.4.2 關鍵問題
- 為什么是四次揮手,不能是三次揮手
- 為什么不能是兩次揮手
- 三、TS通過Socket實現聊天室基礎功能
- 3.1 服務端實現原理
- 3.2 客戶端實現原理
一、進程中如何通信
重要的進程間通信(不同進程之間傳播或交換信息)方式分為六種
管道、信號、消息隊列、共享內存、信號量、socket。其中前五種主要用于一臺主機之中的各個進程之間的通信,socket套接字通信主要用于網絡之中不同主機之間的通信。
1.1 管道
管道(Pipe)其本質是由內核維護的一段內存緩存區。一個進程向該緩存寫入數據,另一個進程從中讀取數據,形成單向數據流。管道傳輸的數據是無格式的字節流,且受內核緩沖區大小的限制。
1.1.1 核心特性
- 單向通信
匿名管道僅支持單向數據傳輸(一端寫入,另一端讀取),若需雙向通信,必須建立兩條獨立的管道。這種單向性體現了其半雙工通信的特性。 - 親緣關系依賴
- 匿名管道:通常用于父子進程或兄弟進程等有親緣關系的進程間通信。子進程通過繼承父進程的文件描述符訪問管道。
- 命名管道(FIFO):通過文件系統中的路徑標識,允許無親緣關系的進程通過打開同一路徑進行通信,突破了匿名管道的親緣限制。
- 阻塞與非阻塞模式
- 默認情況下,讀進程在管道無數據時會阻塞等待;寫進程在緩沖區滿時也會阻塞,直到有空間釋放。
- 可通過
fcntl
函數設為非阻塞模式:讀空管道時直接返回EAGAIN
錯誤,寫滿時丟棄數據或部分寫入。
- 生命周期管理
- 匿名管道隨進程終止自動銷毀。
- 命名管道需手動刪除其文件路徑(如
unlink
),否則會持久存在于文件系統中。
- 容量限制
內核緩沖區大小固定(通常為4KB~64KB)。若寫入速度遠超讀取速度,寫進程可能被長時間阻塞,需設計合理的讀寫協同邏輯。
1.1.2 缺點
- 半雙工通信的天然限制
匿名管道僅支持單向數據傳輸,雙向通信需額外建立一條管道,增加了資源管理和協調的復雜度。 - 讀寫阻塞的強依賴性
若管道內的數據未被讀進程及時消費,寫進程會因緩沖區滿而阻塞,直到讀進程取走數據。這種強同步機制可能導致進程間死鎖(如雙方同時等待對方讀寫)。
1.1.3 匿名管道與命名管道的對比
特性 | 匿名管道 | 命名管道(FIFO) |
---|---|---|
創建方式 | pipe() 系統調用 | mkfifo() 命令或函數 |
通信方向 | 半雙工(單向,需雙向則建兩條) | 半雙工,但支持多進程讀寫 |
進程關系 | 僅限親緣進程 | 任意進程(通過文件路徑訪問) |
持久性 | 隨進程結束銷毀 | 需手動刪除文件路徑 |
注意事項
- 數據原子性:若單次寫入數據量小于
PIPE_BUF
(通常512B~4KB),內核保證寫入的原子性;反之可能被拆分。- 同步問題:共享內存需配合信號量,而管道自身通過阻塞機制隱式同步,但仍需注意讀寫端協調。
- 性能瓶頸:高頻大數據傳輸時,管道可能因拷貝開銷和容量限制成為瓶頸,此時可改用共享內存。
1.2 信號
信號(Signal)是輕量級異步通知機制,由內核或進程向目標進程發送特定事件的通知。其本質是預定義的事件編號(如SIGINT
對應終端中斷),用于觸發進程的默認行為或自定義處理邏輯。
信號是進程間通信中最簡單、最直接的異步通知機制,適用于事件驅動、進程控制等場景。但其設計初衷是“通知”而非“數據傳輸”,因此復雜交互需結合其他IPC機制(如管道、共享內存)。
1.2.1 核心特性
- 異步通知
信號在任意時刻可中斷進程當前操作,直接跳轉到信號處理函數執行,與進程的執行流無關。 - 預定義類型
系統定義了約30種標準信號,編號范圍通常為1~31。 - 處理方式靈活性
- 默認行為:終止進程、忽略信號、暫停進程。
- 自定義處理:通過
signal()
或sigaction()
注冊用戶函數(如處理SIGINT
實現優雅退出)。
- 生命周期
- 生成:由內核、其他進程或終端觸發。
- 傳遞:內核將信號加入目標進程的信號隊列,等待進程調度處理。
- 處理:進程從內核態返回用戶態時,檢查并執行信號處理函數。
1.2.2 缺點
- 信息傳遞能力弱
信號僅能傳遞事件編號,無法攜帶額外數據(實時信號如SIGRTMIN
可攜帶少量信息,但需復雜處理)。 - 信號丟失與覆蓋
- 同類非實時信號多次到達時,可能被合并為一次。
- 處理函數執行期間,新到達的同類型信號可能被阻塞。
- 處理函數的安全限制
信號處理函數需為可重入函數,避免使用非線程安全操作。 - 實時性受限
非實時信號無優先級,內核可能延遲傳遞,無法保證嚴格時序。
1.2.3 信號分類對比
類型 | 非實時信號(標準信號) | 實時信號(SIGRTMIN~SIGRTMAX ) |
---|---|---|
編號范圍 | 1~31 | 34~64(依系統不同) |
隊列機制 | 不排隊,多次發送可能合并 | 支持排隊,按順序處理 |
數據攜帶 | 不支持 | 可通過sigqueue() 附加數據 |
優先級 | 無 | 支持信號優先級 |
注意事項
- 避免處理函數阻塞
信號處理函數應快速完成,復雜邏輯可通過標記位在主線程序處理。- 信號屏蔽與競態條件
- 使用
sigprocmask
屏蔽關鍵代碼段的信號,防止處理函數中斷敏感操作。- 處理共享資源時需考慮信號引發的競態問題。
- 系統調用中斷
信號可能中斷阻塞的系統調用,需檢查錯誤碼EINTR
并重試。- 信號與多線程
多線程程序中,信號可能由任意線程處理,建議統一由主線程接管。
1.3 消息隊列
消息隊列(Message Queue)其本質是由內核維護的鏈表結構,允許進程以消息塊(結構化數據)的形式異步通信。相比管道,消息隊列支持更靈活的格式和隨機讀取,適用于頻繁或結構化的數據交換場景。
消息隊列彌補了管道在結構化數據和異步通信上的不足,適用于中等頻率、結構化消息交換的場景。但其性能瓶頸(上下文切換與拷貝開銷)使其難以應對超高頻需求,此類場景可優先考慮共享內存或Unix域套接字。
1.3.1 核心特性
- 異步非阻塞通信
- 發送進程將消息寫入隊列后立即返回,無需等待接收進程響應。
- 接收進程可主動拉取消息,若隊列為空可選擇阻塞或非阻塞模式。
- 結構化消息
- 消息包含類型標識和數據體,雙方需約定格式。
- 支持按消息類型讀取,而非嚴格FIFO順序。
- 內核持久性
- 消息隊列獨立于進程存在,進程終止后消息仍保留在內核中。
- 可通過權限控制限制其他進程訪問。
- 原子性保證
- 單次寫入的消息若小于
MSGMAX
,內核保證原子性)。
- 單次寫入的消息若小于
- 多進程共享
任意進程(需權限)均可通過隊列標識符訪問同一隊列,支持多對多通信。
1.3.2 缺點
- 消息大小限制
單條消息長度受內核參數MSGMAX
限制(默認約8KB),超出需分片處理。 - 性能開銷
- CPU上下文切換:每次讀寫需通過系統調用進入內核態,頻繁操作時開銷顯著。
- 數據拷貝:消息從用戶空間拷貝到內核隊列,再拷貝到接收方用戶空間,高頻場景效率低。
- 隊列容量限制
隊列總大小受內核參數MSGMNB
限制(默認約16KB~64KB),寫滿后發送進程默認阻塞。 - 復雜性
需自行處理消息類型匹配、分片重組、隊列滿/空等問題,開發復雜度較高。
注意事項
- 消息類型設計
- 類型值應明確區分用途(如正數用于請求,負數用于響應)。
- 避免類型沖突,建議使用枚舉或宏定義。
- 隊列泄露防護
- 確保進程退出前釋放隊列。
- 通過
ipcs -q
和ipcrm
命令管理殘留隊列。- 超長消息處理
- 若消息長度超過
MSGMAX
,需在應用層分片發送,接收端重組。- 信號量同步(可選)
- 多進程競爭讀寫時,可結合信號量實現互斥鎖,避免消息覆蓋。
1.4 共享內存
共享內存(Shared Memory)是進程間通信(IPC)中速度最快的機制,其本質是由內核分配的一段物理內存區域,被多個進程映射到各自的虛擬地址空間中。進程通過直接讀寫該內存區域實現數據交互,無需內核中轉或數據拷貝,從而極大提升通信效率。
共享內存是進程間通信的性能天花板,尤其適合對吞吐量和延遲敏感的場景。但其“直接訪問”的特性如同一把雙刃劍,在提供極致速度的同時,也要求開發者嚴格管理同步與數據一致性。結合信號量、互斥鎖等機制,可構建高效且穩定的多進程協作系統。
1.4.1 核心特性
- 零拷貝高效性
數據直接在共享內存區域讀寫,避免了管道、消息隊列等機制中用戶態與內核態間的數據拷貝開銷。 - 虛擬地址映射
- 每個進程通過頁表將共享內存映射到自身虛擬地址空間的不同位置。
- 進程通過虛擬地址訪問共享內存,由MMU(內存管理單元)完成虛實地址轉換。
- 多進程并發訪問
多個進程可同時映射同一共享內存區域,實現高速數據共享,但需配合同步機制(如信號量、互斥鎖)避免競爭。 - 內核持久性
- 共享內存獨立于進程存在,進程退出后仍保留(除非顯式刪除)。
- 通過
shmctl(IPC_RMID)
銷毀或系統重啟后清除。
1.4.2 缺點
-
同步復雜度高
需額外機制(如信號量)協調讀寫
-
安全隱患
惡意進程可能改寫數據
-
生命周期管理
需顯示刪除避免內存泄漏
注意事項
- 內存對齊與訪問
- 確保數據結構對齊,避免不同進程因編譯差異導致的內存解釋錯誤。
- 緩存一致性
- 多核CPU中,共享內存可能引發緩存一致性問題,需通過內存屏障或原子操作保證可見性。
- 安全與權限控制
- 設置嚴格的IPC權限(如
0666
僅允許同組用戶訪問),防止未授權進程篡改數據。- 資源泄漏防護
- 確保進程退出前調用
shmdt()
和shmctl()
,避免內存段永久占用。
1.5 信號量
信號量(Semaphore)是進程間或線程間同步與互斥的核心工具,其本質是由內核維護的整型計數器,用于協調多個執行單元對共享資源的訪問。信號量的核心思想是通過P
(等待)和V
(釋放)操作,實現資源的原子性分配與釋放,避免競態條件(Race Condition)。
信號量是解決并發編程中同步與資源分配問題的基石,其靈活性使其適用于從簡單互斥到復雜資源管理的廣泛場景。然而,信號量的低級特性也要求開發者對并發邏輯有深刻理解,避免死鎖、饑餓等典型問題。在實際開發中,可優先使用高層抽象(如線程池、無鎖隊列),但在需要精細控制時,信號量仍是不可替代的工具。
1.5.1 核心特性
- 計數器抽象
- 信號量值表示當前可用資源數量:
- 正值:剩余可用資源數。
- 零值:資源已被完全占用,請求者需等待。
- 負值:絕對值表示等待該資源的進程/線程數。
- 信號量值表示當前可用資源數量:
- 原子操作
P操作
(Proberen,嘗試獲取):
若信號量值 > 0,則減1并繼續;否則阻塞等待。V操作
(Verhogen,釋放資源):
信號量值加1,并喚醒一個等待進程。
- 分類
- 二進制信號量:值范圍為0或1,等同于互斥鎖(Mutex)。
- 計數信號量:值范圍≥0,表示資源池容量(如連接池限制)。
- 內核與用戶態實現
- System V信號量:內核維護,支持跨進程同步(如
semget()
)。 - POSIX信號量:可位于共享內存中,支持進程或線程級同步(如
sem_init()
)。
- System V信號量:內核維護,支持跨進程同步(如
1.5.2 缺點
-
死鎖風險
錯誤使用可能導致進程永久阻塞
-
優先級反轉
低優先級進程占用資源,高優先級進程饑餓
-
復雜性
需手動管理信號量創建、初始化和銷毀
注意事項
- 死鎖預防
- 順序一致性:所有進程以相同順序獲取信號量。
- 超時機制:使用
sem_timedwait()
避免無限阻塞。- 信號量泄漏
- System V信號量需顯式調用
semctl(IPC_RMID)
刪除。- POSIX命名信號量需
sem_unlink()
防止殘留。- 原子性與錯誤處理
- 確保
P/V
操作的原子性(如SEM_UNDO
標志應對進程崩潰)。- 檢查
sem_wait()
返回值,處理EINTR
(信號中斷)等錯誤。- 性能優化
- 避免過度使用信號量,高頻場景可結合自旋鎖或無鎖數據結構。
二、Socket
2.1 Socket原理
2.1.1 什么是Socket
在計算機通信領域,socket被翻譯為套接字,他是計算機之間進行通信的一種約定或一種方式。通過socket這種約定,一臺計算機可以接收其他計算機的數據,也可以向其他計算機發送數據。
socket起源于Unix,而Unix/Linux基本哲學之一就是”一切皆文件“,都可以用”打開open -→讀寫write/read–> 關閉close”模式來操作。
我的理解就是Socket就是該模式的一個實現:即socket是一種特殊的文件,一些sokcet函數就是對其他進行的操作(讀寫IO、打開、關閉)。
Socket()函數返回一個整型的Socket描述符,隨后的連接建立、數據傳輸等操作都是通過該Socket實現的。
2.1.2 網絡進程如何通信
我們要理解網絡中進程如何通信,得解決兩個問題:
a、我們要如何標識一臺主機,即怎樣確定我們將要通信的進程是在那一臺主機上運行。
b、我們要如何標識唯一進程,本地通過pid標識,網絡中應該怎樣標識?
解決辦法:
a、TCP/IP協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機
b、傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程),因此,我們利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互
2.1.3 Sokcet如何通信
現在,我們知道了網絡中進程如何進行通信,即利用三元組d[ip地址,協議,端口]可以進行網絡間通信了,那我們應該怎么實現?因此,我們sokcet應運而生,他就是利用三元組解決網絡通信的一個中間件工具,就目前而言,幾乎所有應用程序都是采用socket。
socket通信的數據傳輸方式常用的有兩種:
- SOCK_STREAM:表示面向連接的數據傳輸方式。數據可以準確無誤的達到另一臺計算機,如果損壞或丟失,可以重新發送,但效率相對較慢。常見的http協議就使用了SOCK_STREAM數據傳輸,因為要確保數據的正確性,否則網頁不能正常解析。
- OCK_DGRAM:表示無連接的數據傳輸方式。計算機只管傳輸數據,不作數據校驗,如果數據在傳輸中損壞,或者沒有到達另一臺計算機,是沒有辦法補救的。也就是說,數據錯了就錯了,無法重傳。因為 SOCK_DGRAM 所做的校驗工作少,所以效率比 SOCK_STREAM 高。
2.2 TCP/IP協議
2.2.1 概念
TCP/IP提供點對點的連接機制,將數據應該如何封裝、定址、傳輸、路由以及在目的地如何接收,都加以標準化。它將軟件通信過程抽象化為四個抽象層,采取協議堆棧的方式分別實現出不同通信協議。協議族下的各種協議,依其功能不同,被分別歸屬到四個層次結構中,常被視為是簡化的七層OSI模型。
-
四層結構(由下至上):
- 網絡接口層(鏈路層):負責物理介質的數據幀傳輸(如以太網協議)。
- 網絡層(IP層):通過IP地址實現主機間的邏輯尋址和路由(如IP協議)。
- 傳輸層:提供端到端的數據傳輸服務(如TCP、UDP協議)。
- 應用層:面向用戶提供具體服務(如HTTP、FTP協議)。
-
TCP(傳輸控制協議)是面向連接的、可靠的、基于字節流的傳輸層協議。其核心特性包括:
-
三次握手建立連接:確保雙方通信能力及初始序列號同步。
-
四次揮手釋放連接:保證數據完整性并優雅關閉雙工通道。
-
超時重傳、流量控制、擁塞控制:保障數據傳輸的可靠性。
-
2.2.2 TCP數據報結構
TCP報文頭部固定20字節(不含選項字段),關鍵字段如下:
- 源端口與目的端口(各16位):標識發送方和接收方的應用進程。
- 序號(Seq,32位):本報文段發送數據的第一個字節的編號。
- 確認號(Ack,32位):期望接收的下一個字節的編號,Ack = 收到的Seq + 數據長度 + 1(若數據長度為0,如SYN/FIN標志位,視為占1個序號)。
- 數據偏移(4位):TCP首部長度(以4字節為單位)。
- 標志位(6位):
- URG:緊急指針有效(需配合緊急指針字段使用)。
- ACK:確認號有效(建立連接后所有報文必須置1)。
- PSH:接收方應立即將數據提交應用層。
- RST:強制斷開連接(異常終止)。
- SYN:發起連接請求(同步序列號)。
- FIN:請求終止連接。
- 窗口大小(16位):接收方當前可接受的數據量(流量控制)。
- 校驗和(16位):確保數據完整性。
2.3 連接建立(三次握手)
2.3.1 建立過程
客戶端調用 socket() 函數創建套接字后,因為沒有建立連接,所以套接字處于CLOSED狀態;服務器端調用 listen() 函數后,套接字進入LISTEN狀態,開始監聽客戶端請求
這時客戶端發起請求:
- 當客戶端調用 connect() 函數后,TCP協議會組建一個數據包,并設置 SYN 標志位,表示該數據包是用來建立同步連接的。同時生成一個隨機數字 1000,填充“序號(Seq)”字段,表示該數據包的序號。完成這些工作,開始向服務器端發送數據包,客戶端就進入了SYN-SEND狀態。
- 服務器端收到數據包,檢測到已經設置了 SYN 標志位,就知道這是客戶端發來的建立連接的“請求包”。服務器端也會組建一個數據包,并設置 SYN 和 ACK 標志位,SYN 表示該數據包用來建立連接,ACK 用來確認收到了剛才客戶端發送的數據包
服務器生成一個隨機數 2000,填充“序號(Seq)”字段。2000 和客戶端數據包沒有關系。
服務器將客戶端數據包序號(1000)加1,得到1001,并用這個數字填充“確認號(Ack)”字段。
服務器將數據包發出,進入SYN-RECV狀態 - 客戶端收到數據包,檢測到已經設置了 SYN 和 ACK 標志位,就知道這是服務器發來的“確認包”。客戶端會檢測“確認號(Ack)”字段,看它的值是否為 1000+1,如果是就說明連接建立成功。
接下來,客戶端會繼續組建數據包,并設置 ACK 標志位,表示客戶端正確接收了服務器發來的“確認包”。同時,將剛才服務器發來的數據包序號(2000)加1,得到 2001,并用這個數字來填充“確認號(Ack)”字段。
客戶端將數據包發出,進入ESTABLISED狀態,表示連接已經成功建立。 - 服務器端收到數據包,檢測到已經設置了 ACK 標志位,就知道這是客戶端發來的“確認包”。服務器會檢測“確認號(Ack)”字段,看它的值是否為 2000+1,如果是就說明連接建立成功,服務器進入ESTABLISED狀態。
至此,客戶端和服務器都進入了ESTABLISED狀態,連接建立成功,接下來就可以收發數據了。
2.3.2 關鍵問題
為什么是三次握手,而不是兩次四次?
- 三次握手才可以阻止重復歷史連接的初始化(主要原因)
- 三次握手才可以雙方同步的初始序列號
- 三次握手才可以避免浪費資源
-
阻止重復歷史連接的初始化(主要原因)
-
在兩次握手的情況下,服務端沒有中間狀態給客戶端來阻止歷史連接,導致服務端可能建立一個歷史連接,造成資源浪費。
-
三次握手已滿足上述所有需求,額外增加握手次數(如四次)會引入不必要的延遲,且無法進一步解決核心問題。三次是理論上的最小安全交互次數。
-
-
雙方同步的初始序列號
當客戶端發送攜帶「初始序列號」的
SYN
報文的時候,需要服務端回一個ACK
應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端發送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答回應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。 -
避免資源浪費
如果只有「兩次握手」,當客戶端的 SYN 請求連接在網絡中阻塞,客戶端沒有接收到 ACK 報文,就會重新發送 SYN ,由于沒有第三次握手,服務器不清楚客戶端是否收到了自己發送的建立連接的 ACK 確認信號,所以每收到一個 SYN 就只能先主動建立一個連接,這會造成什么情況呢?
如果客戶端的 SYN 阻塞了,重復發送多次 SYN 報文,那么服務器在收到請求后就會建立多個冗余的無效鏈接,造成不必要的資源浪費。
2.4 斷開連接(四次揮手)
2.4.1 斷連過程
TCP連接的釋放需要四次揮手,其本質是雙向通信的全雙工特性決定的:每個方向必須獨立關閉。以下為詳細過程(以客戶端主動關閉為例):
- 第一次揮手(FIN)
客戶端調用close()
后,發送FIN報文(FIN=1),進入FIN_WAIT_1
狀態,表示客戶端不再發送數據,但仍可接收數據。 - 第二次揮手(ACK)
服務端收到FIN后,立即回復ACK報文,進入CLOSE_WAIT
狀態。此時服務端可能仍有未發送完的數據,客戶端收到ACK后進入FIN_WAIT_2
狀態。 - 第三次揮手(FIN)
當服務端數據發送完畢,準備好關閉連接時,發送FIN報文,進入LAST_ACK
狀態,表示服務端不再發送數據。 - 第四次揮手(ACK)
客戶端收到FIN后,回復ACK報文,進入TIME_WAIT
狀態,等待2MSL(Maximum Segment Lifetime,報文最大生存時間)后關閉連接。服務端收到ACK后立即進入CLOSED
狀態。
2.4.2 關鍵問題
為什么是四次揮手,不能是三次揮手
- 全雙工通信的特性
TCP連接是全雙工的,雙方需獨立關閉自己的數據通道。客戶端發送FIN僅表示其不再發送數據(但可接收),服務端的ACK僅確認收到FIN。服務端的FIN需等待其數據發送完畢后再發送,因此ACK和FIN不能合并為一次。 - 數據完整性保障
若服務端收到FIN后立即合并ACK與FIN(變為三次揮手),可能丟失未傳輸完的數據。分開發送確保服務端有足夠時間處理剩余數據。 - 可靠性設計
客戶端最后的TIME_WAIT
狀態(等待2MSL)有兩個作用:- 確保服務端收到最后的ACK。若ACK丟失,服務端會重傳FIN,客戶端可再次響應。
- 防止舊連接的延遲報文干擾新連接。
為什么不能是兩次揮手
- 服務端未確認自身數據是否已發送完畢。
- 客戶端無法確認服務端是否收到最終ACK,可能造成服務端持續等待。
三、TS通過Socket實現聊天室基礎功能
3.1 服務端實現原理
- 核心結構
-
使用Node.js的
net
模塊創建TCP服務器 -
定義了
Room
類型管理聊天室信息:type Room = {roomName: string; // 房間名稱port: number; // 監聽端口users: [string, net.Socket][]; // 用戶列表([客戶端地址, Socket對象]) };
- 啟動流程
-
關鍵功能實現
-
客戶端連接管理:使用serverConnectEvent處理新連接
-
記錄客戶端地址(client.remoteAddress:client.remotePort)
-
存儲Socket對象到用戶列表
-
-
消息廣播機制:
private broadcast(content: string) {for (const [_, userClient] of this.room.users) {if (userClient.writable) {userClient.write(content); // 向所有客戶端發送消息}} }
-
-
服務端實現示例
// src/server/server.ts import * as net from 'net'; import * as readline from 'readline';// 定義房間類型 type Room = {roomName: string;port: number;users: [string, net.Socket][]; };// 定義服務器類 class MyTCPServer {private server: net.Server;private room: Room;constructor(port: number = 8080, roomName: string = '大廳') {this.room = {roomName,port,users: []};this.server = net.createServer(this.serverConnectEvent.bind(this));this.initServer();this.listenForShutdown()}private initServer() {this.server.listen(this.room.port, () => {console.log(`服務器已啟動,監聽端口 ${this.room.port}`);});this.server.on('close', () => {console.log('服務器已關閉');});}private serverConnectEvent(client: net.Socket) {console.log(`客戶端已連接: ${client.remoteAddress}:${client.remotePort}`);//設計用戶ID為標識const clientId = `${client.remoteAddress}:${client.remotePort}`;// 添加客戶端到用戶列表this.room.users.push([`${client.remoteAddress}:${client.remotePort}`, client]);client.on('data', (chunk) => {const content = chunk.toString();if( content === 'kick') {this.disconnectClient(client)} else {this.broadcast( `${clientId}: ${content}`, client)}});client.on('end', () => {console.log(`客戶端已斷開連接: ${client.remoteAddress}:${client.remotePort}`);this.removeClient(client);});client.on('error', (err) => {console.error(`客戶端發生錯誤: ${err.message}`);this.removeClient(client);});}private broadcast(content: string, sender: net.Socket) {for (const [_, userClient] of this.room.users) {if (userClient.writable && userClient !== sender) {userClient.write(content);}}}private removeClient(client: net.Socket) {const index = this.room.users.findIndex(([_, userClient]) => userClient === client);if (index !== -1) {this.room.users.splice(index, 1);}}//斷開客戶端連接private disconnectClient(client: net.Socket) {const clientInfo = `${client.remoteAddress}:${client.remotePort}`;console.log(`正在斷開客戶端連接: ${clientInfo}`);client.end();this.removeClient(client);}//關閉服務器private listenForShutdown() {const rl = readline.createInterface({input: process.stdin,output: process.stdout});rl.question('輸入 "shutdown" 關閉服務器: ', (input: string) => {if (input === 'shutdown') {// 關閉所有客戶端連接for (const [_, userClient] of this.room.users) {userClient.destroy(); // 強制斷開客戶端}//關閉服務器this.server.close(() => {console.log('服務器已關閉');});rl.close();} else {rl.close();this.listenForShutdown();}});} }// 啟動服務器 new MyTCPServer();
3.2 客戶端實現原理
-
核心結構
- 使用net.Socket連接服務器
- 通過readline模塊實現控制臺的輸入
- 事件驅動架構
-
工作流程
-
關鍵功能實現
-
輸入處理:
private readInput() {this.rl.question('請輸入消息: ', (input) => {this.client.write(input);this.readInput(); // 遞歸調用實現持續輸入}); }
-
消息接收:
this.client.on('data', (chunk) => {const content = chunk.toString();console.log(content); // 直接打印原始消息 });
-
-
客戶端實現示例
// src/client/client.ts import * as net from 'net'; import * as readline from 'readline';// 定義客戶端類 class MyTCPClient {private client: net.Socket;private rl: readline.Interface;constructor() {this.client = new net.Socket();this.rl = readline.createInterface({input: process.stdin,output: process.stdout});this.connectToServer();}private connectToServer() {this.client.connect(8080, () => {console.log('已連接到服務器');this.readInput();});this.client.on('data', (chunk) => {const content = chunk.toString();console.log('你接收到了一條消息\n' + content);});this.client.on('end', () => {console.log('與服務器的連接已斷開');this.rl.close();});this.client.on('error', (err) => {console.error(`與服務器的連接發生錯誤: ${err.message}`);this.rl.close();});}private readInput() {this.rl.question('請輸入消息(輸入"exit"斷開連接):\n ', (input) => {if( input === 'exit') {this.client.end();this.rl.close();} else {this.client.write(input, (err) => {if (err) {console.log('發送消息失敗');} else {console.log('你發出了一條消息\n' + input);}this.readInput();});}});} }// 啟動客戶端 new MyTCPClient();