自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

網(wǎng)絡(luò)編程 | 徹底搞懂網(wǎng)絡(luò) IO 模型

開發(fā) 項(xiàng)目管理
這篇文章從 socket 編程出發(fā),你了解到了怎么利用socket編寫服務(wù)端代碼,然后在 socket 編程時(shí)發(fā)現(xiàn)了痛點(diǎn),一個(gè)在于 accept 建立連接會(huì)阻塞線程,另一個(gè)在于 read 數(shù)據(jù)時(shí)會(huì)阻塞,為了解決阻塞可能導(dǎo)致的低效問題,我們嘗試了用多線程方法來初步解決。

令人頭大的 IO

說起網(wǎng)絡(luò) IO 相關(guān)的開發(fā),很多人都頭大,包括我自己,寫了幾年的代碼,對(duì) IO 相關(guān)的術(shù)語說起來也是頭頭是道,什么 NIO、IO 多路復(fù)用等術(shù)語一個(gè)接一個(gè)。但是也就自己知道,這些概念一團(tuán)亂,網(wǎng)上各種各樣的文章也沒一個(gè)權(quán)威易懂的,并且很多文章說起 IO 就扯上 Java 的 NIO 包,專注的大多是如何使用(術(shù))而不是 IO 的本質(zhì)(道)。所以寫這篇文章來從 socket 編程的痛點(diǎn),轉(zhuǎn)到 NIO 的解決方案,再到多路復(fù)用器的發(fā)展來一起梳理網(wǎng)絡(luò)IO 模型。

從 Socket 編程說起

做業(yè)務(wù)開發(fā)的同學(xué),常常面對(duì)的是 Spring Boot 這些框架幫我們搭建好的 Server 框架,但是如果往下去看框架幫我們實(shí)現(xiàn)的代碼最終會(huì)看到 Socket 相關(guān)的源碼, Socket 相關(guān)的代碼實(shí)際上就是 TCP 網(wǎng)絡(luò)編程。

目前主流的 HTTP 框架,比如 Golang 原生的 HTTP net/http,都是基于 TCP 編程實(shí)現(xiàn)的,按照 HTTP 協(xié)議約定,解析 TCP 傳輸流過來的數(shù)據(jù),最終將傳輸數(shù)據(jù)轉(zhuǎn)換為一個(gè) Http Request Model 交給我們業(yè)務(wù)的 Handler 邏輯處理。

例如,Golang 的 原生 Http 框架 net/http 為例就有這么些代碼片段:

l, err = sl.listenTCP(ctx, la) // 監(jiān)聽連接請(qǐng)求

rw, e := l.Accept() // 創(chuàng)建連接

go c.serve(connCtx) // 調(diào)用新的協(xié)程處理請(qǐng)求邏輯

w, err := c.readRequest(ctx) // 讀取請(qǐng)求

serverHandler{c.server}.ServeHTTP(w, w.req) // 執(zhí)行業(yè)務(wù)邏輯,并返回結(jié)果

Socket 編程的過程

  1. 服務(wù)端需要先綁定(Bind)并監(jiān)聽(Listen)一個(gè)端口,這個(gè)時(shí)候會(huì)有一個(gè)歡迎套接字(welcomeSocket)
  2. welcomeSocket 調(diào)用 Accept 方法,接受客戶端的請(qǐng)求,如果沒有請(qǐng)求那么會(huì)阻塞住
  3. 客戶端請(qǐng)求指定端口,welcomeSocket 從阻塞中返回一個(gè)已連接套接字(connectionSocket)用于專門處理這個(gè)客戶端請(qǐng)求
  4. 客戶端往請(qǐng)求套接字寫入數(shù)據(jù)(Stream)
  5. 服務(wù)端從已連接套接字可以持續(xù)讀到數(shù)據(jù),TCP 底層保證數(shù)據(jù)的順序性
  6. 服務(wù)端可以往已連接套接字寫入數(shù)據(jù),客戶端從請(qǐng)求的套接字中可以讀到數(shù)據(jù)
  7. 客戶端關(guān)閉連接,服務(wù)端也可以主動(dòng)關(guān)閉連接

如果用代碼手寫 Socket 服務(wù)端,用 Java 實(shí)現(xiàn)是這樣的:

public class Server {
public static void main(String[] args) throws Exception {
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = new ServerSocket(6789);
while (true) {
Socket connectionSocket = welcomeSocket.accept(); // 當(dāng)沒請(qǐng)求會(huì)阻塞住
System.out.println("connection build succ!");
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine(); // 連接上但是客戶端還沒寫入數(shù)據(jù)會(huì)阻塞住
System.out.println("read succ!");
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
System.out.println("write succ!");
}
}
}

我們可以用 Telnet 連接上去嘗試下,但是很快我們會(huì)發(fā)現(xiàn)兩個(gè)問題:

  • Accept 是阻塞的,如果一個(gè)客戶端網(wǎng)絡(luò)比較差,三次握手時(shí)間長整個(gè)服務(wù)端就卡住了。
  • Read 是阻塞的,如果客戶端連接上了,但是遲遲不發(fā)數(shù)據(jù)(比如我們 telnet 上,但是不寫)整個(gè)服務(wù)端就卡住了。

優(yōu)化思路:多線程處理,避免 read 阻塞

對(duì)于 Read 是阻塞的問題,我們開線程來處理,這樣當(dāng)一個(gè)請(qǐng)求連接上遲遲不寫數(shù)據(jù)也不會(huì)影響到其他連接的處理了。當(dāng)然這里得考慮到量級(jí),如果量級(jí)太大的話需要改成線程池避免線程過多。

public class Server {
public static void main(String[] args) throws Exception {
ServerSocket welcomeSocket = new ServerSocket(6789);
while (true) {
Socket connectionSocket = welcomeSocket.accept(); // 當(dāng)沒請(qǐng)求會(huì)阻塞住
System.out.println("connection build succ!");
new Thread(new Runnable() {
@Override
public void run() {
try {
String clientSentence;
String capitalizedSentence;

BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());

clientSentence = inFromClient.readLine(); // 連接上但是客戶端還沒寫入數(shù)據(jù)會(huì)阻塞住
System.out.println("read succ!");

capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
System.out.println("write succ!");
}catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}

這個(gè)時(shí)候,我們用 telnet 客戶端連接已經(jīng)感受不到服務(wù)端的瓶頸了。但是從我們上的分析來看,Accept 還是有瓶頸的,就是同時(shí)只能對(duì)一個(gè)請(qǐng)求做連接,而且即使是線程池的模式,如果連接(特別是空閑的)很多,最終也會(huì)出現(xiàn)阻塞的情況。

如果你的業(yè)務(wù)場(chǎng)景是連接數(shù)不多,同時(shí)又需要頻繁的交互數(shù)據(jù),那么用 BIO 模式無論是對(duì)時(shí)延還是資源使用都有不錯(cuò)的效果(相當(dāng)于 VIP 1v1 服務(wù))。

但是,我們的服務(wù)端代碼通常是面對(duì)海量的連接的,且很多客戶端連接上,并不會(huì)馬上發(fā)送請(qǐng)求,例如聊天室應(yīng)用,很久用戶才會(huì)發(fā)送1條消息。這個(gè)時(shí)候如果還是這種 1v1 模式,那么有 100w 個(gè)用戶,就需要維護(hù) 100個(gè)連接,顯然是不合適,這太浪費(fèi)資源了,而且很低效,大部分線程都是在 Block 等待用戶數(shù)據(jù)。

所以,這個(gè)時(shí)候 NIO(異步io)橫空出世了。網(wǎng)上有一張比較好的對(duì)比圖,可以很好地解釋差異:

我們上文說的就是 阻塞I/O ,而現(xiàn)在要講的是非阻塞I/O。圖上主要闡述的是 `read()` 方法的過程,主要包括兩部分:

第一階段:等待TCP RecvBuffer 數(shù)據(jù)就緒,這個(gè)在傳統(tǒng)的BIO里如果數(shù)據(jù)沒就緒,就會(huì)阻塞等待,不消耗CPU。

第二階段:將數(shù)據(jù)從內(nèi)核拷貝到用戶空間,消耗CPU但是速度非???,屬于 memory copy。

非阻塞I/O

所以對(duì)于 非阻塞I/O 來說,主要要優(yōu)化的是調(diào)用 `read()` 方法數(shù)據(jù)還未就緒導(dǎo)致阻塞問題。這個(gè)解決方法很簡單,大部分編程語言都有提供 nio 的方法,只要數(shù)據(jù)還沒準(zhǔn)備就緒不要block,直接返回給調(diào)用者就可以了。這樣我們這個(gè)線程就可以接著去處理其他連接的數(shù)據(jù),這樣就不用每個(gè)連接單獨(dú)只有一個(gè)線程來服務(wù)了。

I/O 多路復(fù)用

對(duì)于非阻塞 I/O 模式,開發(fā)者仍然需要不斷去輪詢事件狀態(tài),如果請(qǐng)求量級(jí)很大, 這樣的機(jī)制同樣還是會(huì)浪費(fèi)很多資源,同時(shí)開發(fā)難度較高。其實(shí)想一想,我們作為開發(fā)者的訴求無非就是監(jiān)聽某些事件,比如完成鏈接(accept完成)、數(shù)據(jù)就緒(可read)等。關(guān)于事件的監(jiān)聽其實(shí)也無關(guān)乎編程語言,在操作系統(tǒng)層面就可以做而且可以做的更高效。操作系統(tǒng)上提供了一系列系統(tǒng)調(diào)用,比如 select/poll/epool,這些系統(tǒng)調(diào)用后會(huì)阻塞,當(dāng)有對(duì)應(yīng)的事件到來觸發(fā)我們注冊(cè)到事件上的Handler邏輯。

所以簡單來說,就是上文說的 非阻塞I/O 用戶自行寫輪詢查看狀態(tài)的邏輯被收斂到操作系統(tǒng)這里提供的 I/O 復(fù)用器了,整個(gè)程序執(zhí)行起來的邏輯大概變成這樣。

 interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//讀,寫或者連接
}
//IO線程主循環(huán):
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//選擇就緒的事件和對(duì)應(yīng)的連接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新連接,則注冊(cè)一個(gè)新的讀寫處理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以寫,則執(zhí)行寫事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以讀,則執(zhí)行讀事件
}
}
}
Map<ChannelChannelHandler> handlerMap;//所有channel的對(duì)應(yīng)事件處理器
}

Reactor 模型

目前大多高性能的網(wǎng)絡(luò)IO框架主要都是基于IO多路復(fù)用 + 池化技術(shù)的的 Reactor 模型,Reactor 其實(shí)只是一個(gè)網(wǎng)絡(luò)模型概念并不是具體的某項(xiàng)具體技術(shù)。常見的主要有三種,單Reactor + 單進(jìn)程/單線程、單Reactor + 多線程、多Reactor + 多進(jìn)程/多線程。

單Reactor + 單進(jìn)程/單線程

多路復(fù)用器 Select 返回結(jié)果后,有個(gè) Dispatch 用于分發(fā)結(jié)果事件。如果是連接建立事件,Acceptor接受連接并創(chuàng)建對(duì)應(yīng)的Handler來處理后續(xù)事件。如果不是連接事件,直接調(diào)用對(duì)應(yīng)的 Handler,Handler 完成數(shù)據(jù)讀取 read 、process、send 的完整業(yè)務(wù)流程。

這種模式優(yōu)點(diǎn)是簡單、不用考慮進(jìn)程間通信、線程安全、資源競(jìng)爭(zhēng)等問題,但是也有自身局限性,也就是無法充分利用多核資源,適用于業(yè)務(wù)場(chǎng)景處理很快的場(chǎng)景,比如 Redis 就是用這種方案。

單Reactor + 多線程

相比于上一種方案,不同的是 Handler 只負(fù)責(zé)數(shù)據(jù)讀取不負(fù)責(zé)處理事件,而是有一個(gè)單獨(dú)的 Worker 線程池來做具體的事情。之所以 processor 要隔離單獨(dú)的線程池是因?yàn)?`read` 方法本身是需要消耗 cpu 資源的,通常不適合大于 cpu 核數(shù),而用戶自定義的 processor 邏輯里可能有各種網(wǎng)絡(luò)請(qǐng)求,比如 RPC 請(qǐng)求,如果隔離開來,那么 processor 可以設(shè)置更大的線程數(shù),提升吞吐量。

這種模式已經(jīng)可以比較充分利用到多核資源了,但是問題在于主線程承擔(dān)了所有的事件監(jiān)聽和響應(yīng)。瞬間高并發(fā)時(shí)可能成為瓶頸,這就需要多 Reactor 的方案了。

多Reactor + 多進(jìn)程/多線程

處理步驟:

  1. 父進(jìn)程中 mainReactor 對(duì)象通過 select 監(jiān)控連接建立事件,收到事件后通過 Acceptor接收,將新的連接分配給某個(gè)子進(jìn)程。
  2. 子進(jìn)程的 subReactor 將 mainReactor 分配的連接加入連接隊(duì)列進(jìn)行監(jiān)聽,并創(chuàng)建一個(gè)Handler 用于處理連接的各種事件。
  3. 當(dāng)有新的事件發(fā)生時(shí),subReactor 會(huì)調(diào)用連接對(duì)應(yīng)的 Handler 來進(jìn)行響應(yīng)。
  4. Handler 完成 read→處理→send 的完整業(yè)務(wù)流程。

目前著名的開源系統(tǒng) Nginx 采用的是多 Reactor 多進(jìn)程,采用多 Reactor 多線程的實(shí)現(xiàn)有Memcache 和 Netty。不過需要注意的是 Nginx 中與上圖中的方案稍有差異,具體表現(xiàn)在主進(jìn)程中并沒有mainReactor來建立連接,而是由子進(jìn)程中的subReactor建立。

異步非阻塞 I/O

服務(wù)器實(shí)現(xiàn)模式為一個(gè)有效請(qǐng)求一個(gè)線程,客戶端的I/O請(qǐng)求都是由OS先完成了再通知服務(wù)器應(yīng)用去啟動(dòng)線程進(jìn)行處理,AIO又稱為NIO2.0,在JDK7才開始支持。但是由于 Linux 上 AIO 的底層實(shí)現(xiàn)并不好,所以目前沒有被廣泛使用。比如大名鼎鼎的Netty框架也是使用NIO而非AIO。

總結(jié)

這篇文章從 socket 編程出發(fā),你了解到了怎么利用socket編寫服務(wù)端代碼,然后在 socket 編程時(shí)發(fā)現(xiàn)了痛點(diǎn),一個(gè)在于 accept 建立連接會(huì)阻塞線程,另一個(gè)在于 read 數(shù)據(jù)時(shí)會(huì)阻塞,為了解決阻塞可能導(dǎo)致的低效問題,我們嘗試了用多線程方法來初步解決。

但是在這之后,我們又看到面對(duì)海量連接時(shí),BIO 力不從心的現(xiàn)象,所以引入了 NIO 模型。這里闡述了從 NIO 到 多路復(fù)用器的進(jìn)步,相當(dāng)于是操作系統(tǒng)幫我們做了海量連接事件的監(jiān)聽,這個(gè)模式也被稱作 Reactor 模式。最后講到了異步I/O,雖然理想很美好,但是底層基建并不完善,目前這種模式在生產(chǎn)中被使用還比較少。

我寫這篇文章,并沒有描述很多具體的API,因?yàn)槲蚁Mㄟ^這個(gè)文章來幫助大家真正了解IO模型的本質(zhì),而不是羅列topic,或者硬性記憶API,因?yàn)榫幊陶Z言很多,而解決方案的思想是統(tǒng)一的。這也是我們學(xué)習(xí)應(yīng)該注意的,更多的應(yīng)該學(xué)其道,而不是學(xué)其術(shù)。

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2020-10-14 08:50:38

搞懂 Netty 線程

2022-02-21 10:21:17

網(wǎng)絡(luò)IO模型

2024-08-08 14:57:32

2025-03-31 08:50:00

模型量化神經(jīng)網(wǎng)絡(luò)AI

2020-09-23 12:32:18

網(wǎng)絡(luò)IOMySQL

2023-02-27 07:22:53

RPC網(wǎng)絡(luò)IO

2022-07-19 16:03:14

KubernetesLinux

2020-04-15 08:00:00

計(jì)算機(jī)網(wǎng)絡(luò)通信設(shè)備通信協(xié)議

2025-04-21 04:00:00

2024-09-04 16:19:06

語言模型統(tǒng)計(jì)語言模型

2023-05-09 11:13:09

IO模型語言

2024-01-03 13:39:00

JS,Javascrip算法

2023-10-18 10:55:55

HashMap

2025-01-13 16:00:00

服務(wù)網(wǎng)關(guān)分布式系統(tǒng)架構(gòu)

2025-04-11 05:55:00

2021-09-30 07:26:15

磁盤IO網(wǎng)絡(luò)

2017-12-05 17:44:31

機(jī)器學(xué)習(xí)CNN卷積層

2023-11-01 11:07:05

Linux高性能網(wǎng)絡(luò)編程線程

2019-02-17 10:05:24

TCPSocket網(wǎng)絡(luò)編程

2016-08-04 15:10:12

服務(wù)器虛擬化網(wǎng)絡(luò)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)