深造分布式 打败面试官 招式三 直捣黄龙
👉🏻👉🏻👉🏻👉🏻上文:深造分布式 打敗面試官 招式二
👉🏻👉🏻👉🏻👉🏻分布式系列訂閱:分布式系列
👏🏻👏🏻👏🏻👏🏻:可關注 評論 及時交流反饋 不斷努力 一起加油 可留下你的👣
👉🏻👉🏻👉🏻👉🏻個人主頁: 個人主頁
👉🏻👉🏻👉🏻👉🏻個人介紹:開發小趴菜 拒絕加班的程序員 為了不加班 只能加油。頹廢又積極的矛盾體 😬
網絡通信:如何完成客戶端和服務端之間的高效通信?
- 前言
- 網絡通信
- 問題背景
- 問題分析
- 技術體系
- 網絡連接
- IO 模型
- 可靠性
- 源碼解析
- Dubbo 服務器端通信原理
- Dubbo 客戶端通信原理
- 要點
- 小結
- 結尾歌
前言
在上一文中,簡單的對構建分布式服務所需要具備的技術組件進行了分析,并把這些技術組件分成三大類,即遠程過程調用組件、微服務構建組件以及通用技術組件 相信大家也會有一些get 到的地方 下面我們就要在進行細化的共同學習 一下 遠程過程調用類組建的使用和討論 首先我們需要了解的是 網絡通信。
網絡通信
分布式系統的構建依賴網絡通信。相比單塊系統的函數式調用,分布式環境下的請求和響應過程涉及到客戶端和服務器端之間跨網絡的交互和協作。這個過程一方面要考慮到網絡的三特性(及時性、交互性、社交性),另一方面也需要考慮資源的利用效率。
那么,如何設計并實現高效的網絡通信機制?這是分布式服務框架的核心功能之一,也是我們進階的時候經常遇到的問題。本文內容將圍繞這一問題展開討論。
問題背景
在日常開發過程中,每一次分布式請求都會涉及到網絡通信。網絡通信表現為一個復雜且不可控的過程。然而,因為像 Dubbo、Spring Cloud 等主流的分布式服務框架都已經幫我們封裝了網絡通信過程,使得遠程過程調用就像是在使用本地方法調用一樣,導致了開發人員對網絡通信過程的底層設計思想和實現原理往往不甚了解, 這一點不知道大家有沒有感觸 有時候我們去用一些特別便捷的框架 對于探究思想 就比較難 隨著技術的發展 我們所使用的技術也越來越成熟 但是不了解設計思想 就是底盤不穩的。
另一方面,對于分布式服務構建過程而言,網絡通信是一個基礎性、通用性的技術主題,涉及廣泛的技術體系,既有深度又有廣度,非常適合考查一個人的知識面
關于網絡通信有很多種具體的問法,有些側重于具體某一個知識點,有些則關注整個通信流程。當然,因為日常開發中大家都是使用一些開源框架來開發分布式系統,因此關于開源框架中網絡通信具體實現過程和底層原理也是常見的考查方式。
這里我梳理了比較有代表性的一些話題,如下所示:
- 網絡的長連接和短連接分別指什么?它們分別有什么特點和優勢?
- 常見網絡 IO 模型有哪些?各有什么功能特性?
- 如果確保網絡通信過程的可靠性?
- 你認為網絡通信包含的核心技術組件有哪些?
- 如果讓你來設計網絡傳輸協議,你有什么樣的一些思考?
- 你能描述 Dubbo 框架中客戶端和服務器端的網絡通信實現過程嗎?
- Dubbo 框架對網絡通信過程采用了什么樣的分層設計思想?
問題分析
網絡通信的實現方式實際上有很多種,但因為網絡通信過程復雜且不可控,因此如何使這個過程的代價降到用戶可以接受的層次是分布式系統設計的重要目標。
我們知道客戶端和服務器端之間需要完成跨網絡的交互過程,也就意味著兩者之間需要建立網絡連接。網絡連接的創建和維護方式決定了通信過程的效率。同時,我們知道任何網絡請求的處理過程都涉及到 IO 操作,而不同類似的 IO 操作方式對性能的影響巨大。在網絡通信過程中,我們需要選擇合適的 IO 模型。
關于網絡通信,可靠性是一個不得不提的話題**。網絡狀態是不穩定的,網絡之間的通信過程必須在發生問題時能夠快速感知并修復**。
我們把上述分析點整理成一張圖,如下所示:
上圖構成啦這類問題的基本思路。
技術體系
在本文內容中,讓我們來對上圖中所展示的各項技術體系展開討論。
網絡連接
關于網絡連接,我們要討論的主要是長連接和短連接的概念。當客戶端在向服務端發送請求并獲取響應結果之后,這兩種連接方式的區別在于對連接本身的處理方式。
所謂短連接,是指一旦請求響應過程結束,連接自動關閉。
而長連接則不同,客戶端可以利用這個連接持續地發送請求并獲取響應結果。
顯然,采用何種連接方式沒有統一的標準,而是要看具體的應用場景。有時候,考慮到性能和服務治理等因素,我們也會把短連接和長連接組合起來使用。
IO 模型
任何網絡請求處理過程都涉及到 IO 操作。
最基本的 IO 操作模型就是阻塞式 IO(Blocking IO,BIO),該模型要求服務器端針對每次客戶端請求都生成一個處理線程,因此對服務器端的資源消耗要求很高。
非阻塞 IO(Non-Blocking IO,NIO)和 IO 復用(IO Multiplexing)技術實際上也會在 IO 上形成阻塞,真正在 IO 上沒有形成阻塞的是異步 IO(Asynchronous,AIO)。
各個 IO 模型效果如下圖所示:
可靠性
因為網絡狀態是不穩定的,所以誕生了一系列手段來確保網絡通信的可靠性。
首先,我們需要對網絡的通信鏈路進行有效性的檢測,這方面最常見的實現機制就是心跳檢測。我們可以在網絡協議的 TCP 層發送心跳包,也可以在應用層協議上傳遞包含一定業務數據的心跳信息。
另一方面,一旦檢測到網絡通信出現問題,那么就需要采取一定措施來恢復網絡狀態。這方面主流的做法就是斷線重連。針對不同場景,我們可以采用不同的重連次數和頻率,直到網絡重連成功或者超時。
看到這里,我們需要注意,上述關于網絡通信相關技術體系的討論都是相對抽象的內容。我們還需要更加具體的依靠,就需要依托一定的開源框架。只有通過這些開源框架,我們才能了解網絡通信的底層原理。
在接下來的內容中,我們將基于目前主流的分布式服務框架 Dubbo 來分析該框架針對網絡通信采用了什么樣的設計思想和實現過程。
源碼解析
在 Dubbo 框架中,存在一個獨立的 Remoting 模塊,封裝了對整個網絡通信的實現過程
Remoting :
在Remoting中是通過通道(channel)來實現兩個應用程序和域之間對象的通信的。首先,客戶端通過Remoting,訪問通道以獲得服務端對象,再通過代理解析為客戶端對象。這就提供一種可能性,即以服務的方式來發布服務器對象。遠程對象代碼可以運行在服務器上(如服務器激活的對象和客戶端激活的對象),然后客戶端再通過Remoting連接服務器,獲得該服務對象并通過序列化在客戶端運行。
在Remoting中,對于要傳遞的對象,設計者除了需要了解通道的類型和端口號之外,無需再了解數據包的格式。這既保證了客戶端和服務器端有關對象的松散耦合,同時也優化了通信的性能。
該模塊的組成結構如下圖所示:
從組成結構上講,Remoting 模塊主要包含三個組件。
Exchange 組件。 信息交換層,用來封裝請求-響應過程。
Transport 組件。 網絡傳輸層,基于 Netty 等框架抽象統一的網絡通信接口。
Serialize 組件。 序列化層,主要完成數據的序列化和反序列化過程。
而從技術分層上講,Remoting 模塊處于整個 Dubbo 框架的底層,是我們后續要介紹的服務發布和服務消費的基礎。顯然,Remoting 模塊的組件呈現的是一種對稱結構,即 Dubbo 的生產者和消費者都依賴于底層的網絡通信。所以,我們也將分別從服務器端和客戶端兩個角度出發分析 Dubbo 中具體的網絡通信過程。
然后,在 Dubbo 中**,真正實現網絡通信的過程委托給了第三方組件。Dubbo 通過 SPI 的方式提供了與 Netty、Mina 等多種通信框架的集成方式。這部分內容相當于是對上圖中 Remoting 模塊的補充,從層次上講應該屬于 Dubbo 框架的最底層**。
Dubbo 服務器端通信原理
我們首先介紹 Dubbo 服務器端的網絡通信過程。請記住,Dubbo 中服務器端通信的目的就是集成并綁定 Netty 服務從而啟動服務監聽。我們關注 Exchange 信息交換層和 Transport 網絡傳輸層這兩個核心層之間的交互和協作過程,如下圖所示:
上圖中涉及了 ExchangeServer、HeaderExchange、NettyTransport 和 NettyServer 等一系列核心對象。顯然,通過這些對象從命名上就可以很明確地將它們劃分到 Exchange 和 Transport 這兩個層。
在 Dubbo 中存在一個 Protocol 接口,該接口是 Dubbo 中最基本的遠程過程調用實現接口,完成了服務的發布和調用功能。而在 Protocol 接口中存在 export 和 refer 這兩個核心方法,其中前者用于對外暴露服務,后者則用來對遠程服務進行引用。針對 Protocol 接口,Dubbo 提供了一組實現類,其中最重要的就是 DubboProtocol。
我們從 DubboProtocol 中的 export 方法進行切入,該方法會根據傳入的 URL 對象創建一個 Exchange 服務器,如下所示:
private void openServer(URL url) {String key = url.getAddress();boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);if (isServer) {ExchangeServer server = serverMap.get(key);if (server == null) {serverMap.put(key, createServer(url));} else {server.reset(url);}} }我們來看 Exchange 服務器的創建過程,對應的 createServer 方法如下所示:
private ExchangeServer createServer(URL url) {// 省略其他代碼ExchangeServer server;try {server = Exchangers.bind(url, requestHandler);} return server; }可以看到這里的關鍵代碼就是通過 Exchangers 的 bind 方法創建了 ExchangeServer,這個過程依賴于一個 Exchanger 接口。那么這個 Exchanger 接口是做什么用的呢?該接口定義如下所示:
@SPI(HeaderExchanger.NAME) public interface Exchanger {@Adaptive({Constants.EXCHANGER_KEY})ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException;@Adaptive({Constants.EXCHANGER_KEY})ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException; }Exchanger 接口只有兩個方法,一個是面向服務器端的 bind 方法,一個是面向客戶端的 connect 方法。在 Dubbo 中,Exchanger 的實現類只有一個,即 HeaderExchanger,如下所示:
public class HeaderExchanger implements Exchanger {public static final String NAME = "header";public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);}public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));} }請注意,上述 HeaderExchanger 在創建 HeaderExchangeServer 的同時也加入心跳檢測功能,如下所示:
private void startHeatbeatTimer() {stopHeartbeatTimer();if (heartbeat > 0) {heatbeatTimer = scheduled.scheduleWithFixedDelay(new HeartBeatTask(new HeartBeatTask.ChannelProvider() {public Collection<Channel> getChannels() {return Collections.unmodifiableCollection(HeaderExchangeServer.this.getChannels());}}, heartbeat, heartbeatTimeout),heartbeat, heartbeat, TimeUnit.MILLISECONDS);} }在 HeartBeatTask 的 run 方法中,我們根據所配置的 heartbeat、heartbeatTimeout 等相關心跳屬性,執行 channel.reconnect、channel.close 等方法。這也是心跳檢測機制的一種常見實現方式。
注意到,我們在 HeaderExchangeServer 中終于看到了屬于 Transport 層的對象 Transporters,接下來我們來看服務器端 Transport 相關的組件。
站在服務器的角度,網絡通信過程的目的只有一個,就是裝配服務并啟動監聽,從而接收來自服務消費者的訪問。而對于服務消費者而言,通信過程的目的無非是對遠程服務進行連接、發送請求并獲取響應。因此,在 Dubbo 中存在一個 Transporter 接口,該接口提供了 bind 和 connect 方法分別對這兩個基本操作進行封裝,如下所示:
@SPI("netty") public interface Transporter {@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})Server bind(URL url, ChannelHandler handler) throws RemotingException;@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})Client connect(URL url, ChannelHandler handler) throws RemotingException; }我們發現 Transporter 接口的定義與 Exchanger 接口非常類似,兩者同時提供了 bind 和 connect 這兩個方法。區別在于 Exchanger 中用到的 ExchangeHandler,而 Transporter 中用到的是 ChannelHandler,顯然 ChannelHandler 面向消息通信的通道,提供了比 ExchangeHandler 更底層的操作語義。
與 HeaderExchanger 不同,Dubbo 中針對 Transporter 接口提供了一批實現類,包括 GrizzlyTransporter、MinaTransporter 以及兩個 NettyTransporter。系統默認會加載 org.apache.dubbo.remoting.transport.netty 包下的 NettyTransporter,該類如下所示:
public class NettyTransporter implements Transporter {public static final String NAME = "netty4";public Server bind(URL url, ChannelHandler listener) throws RemotingException {return new NettyServer(url, listener);}public Client connect(URL url, ChannelHandler listener) throws RemotingException {return new NettyClient(url, listener);} }這里看到了真正實現網絡通信的 NettyServer 類,NettyServer 實現了 Server 接口并擴展了 AbstractServer 類。在 AbstractServer 中,Dubbo 提供了對網絡服務端的通用抽象,即抽象出 open、close、send 等一組面向網絡通信的通用方法。而 NettyServer 作為 AbstractServer 的子類,它的啟動監聽實現代碼如下所示:
protected void doOpen() throws Throwable {...bootstrap = new ServerBootstrap(channelFactory);final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);channels = nettyHandler.getChannels();bootstrap.setOption("child.tcpNoDelay", true);bootstrap.setPipelineFactory(new ChannelPipelineFactory() {public ChannelPipeline getPipeline() {NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);ChannelPipeline pipeline = Channels.pipeline();pipeline.addLast("decoder", adapter.getDecoder());pipeline.addLast("encoder", adapter.getEncoder());pipeline.addLast("handler", nettyHandler);return pipeline;}});channel = bootstrap.bind(getBindAddress()); }熟悉 Netty 框架的開發人員對上述代碼一定不會陌生,這里使用 Netty 的 ServerBootstrap 完成服務啟動監聽,同時構建了 NettyHandler 作為其網絡事件的處理器。然后 NettyServer 的 doClose 方法基于 Netty 的 boostrap 和 channel 對象完成了網絡資源釋放。
這樣我們對 Dubbo 中與網絡通信相關的服務監聽啟動和關閉,以及發送消息的過程就有了整體的了解。
Dubbo 客戶端通信原理
有了對前面服務器端各個技術組件的介紹,理解 Dubbo 客戶端通信原理就會容易很多。我們在介紹服務器端時所引入的 Transporter 接口同時包含了對客戶端方法的定義,而 Transporter 的實現類 NettyTransporter 也同時提供了 NettyClient 類,如下所示:
public class NettyTransporter implements Transporter {public static final String NAME = "netty";public Server bind(URL url, ChannelHandler listener) throws RemotingException {return new NettyServer(url, listener);}public Client connect(URL url, ChannelHandler listener) throws RemotingException {return new NettyClient(url, listener);} }與 NettyTransporter 相關的這些核心類之間的關系下圖所示:
上圖中的很多類我們都已經明確了,因此在介紹 Dubbo 的客戶端通信原理時,不會像服務端那樣做全面展開,而是更多關注于客戶端本身的特性,所以我們的思路先從底層的 NettyClient 類進行切入。
與 NettyServer 類類似,NettyClient 也存在一個抽象的父類 AbstractClient。作為網絡客戶端的通用抽象,AbstractClient 這個模板類一共提供了 doOpen、doConnect、doDisconnect、doClose、getChannel 這 5 個模板方法。與服務器端所提供的 3 個方法相比,客戶端還需要實現與 Connect 這一操作相關的兩個方法。
NettyClient 中的 doOpen 方法如下所示,這里創建了 ClientBootstrap 并完成初始化參數設置。
@Override protected void doOpen() throws Throwable {...bootstrap = new ClientBootstrap(channelFactory); final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);bootstrap.setPipelineFactory(new ChannelPipelineFactory() {public ChannelPipeline getPipeline() {NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);ChannelPipeline pipeline = Channels.pipeline();pipeline.addLast("decoder", adapter.getDecoder());pipeline.addLast("encoder", adapter.getEncoder());pipeline.addLast("handler", nettyHandler);return pipeline;}}); }和 NettyServer 一樣,NettyClient 同樣完成了對 Netty 框架的封裝。在 NettyClient 的 doConnect 方法中,同樣使用 ClientBootstrap 完成與服務端的連接和事件監聽。而 doDisconnect 方法則用于移除當前已經斷開連接的 Channel。然后,和 HeaderExchangeServer 類類似,在 HeaderExchangeClient 類中也添加了定時心跳收發及心跳超時監測機制。
要點
對于遠程過程調用而言,網絡通信還是一個偏向于概念的技術。因此,了解這類型的知識,第一步需要針對網絡通信這個話題本身給出一些說明和描述,這屬于理論知識體系,是需要死記硬背的基礎內容。基于對這一話題的了解,通常我們就可以引出一些自己比較熟悉的點,圍繞這些點來進行展開和引導即可。例如,對于網絡傳輸協議,如果你了解 ISO/OSI 模型,那就可以基于這個模型來對類似“消息頭”這種自己比較熟悉的點進行發散。
第二個要點是對網絡通信實現過程進行抽象化、系統化的闡述。在目前主流的分布式服務框架中,對于網絡通信模塊都會有自己的一些抽象和提煉,其內部結構往往比較復雜。以本講中介紹的 Dubbo 框架為例,就采用了明確的分層架構。每一層中都包含了一些核心的接口,這些接口之間的繼承關系以及所處于的層次如下圖所示:
針對網絡通信,我們要明確 Dubbo 框架集成了很多第三方工具,這部分工具只關注于底層的具體網絡通信過程。Dubbo 把這一過程抽象成一個 Transport 層,即網絡傳輸層。而 Dubbo 又提供了一個 Exchange 層,用于封裝請求和響應,稱為信息交換層。從功能職責上講,Exchange 偏向于 Dubbo 對自身通信過程的抽象和封裝,跟具體的網絡通信關系不大,具體的網絡傳輸工作都是由 Transport 層負責完成。理解 Dubbo 框架的這種設計思想和實現方式,一方面有助于提升面試內容的豐富程度,另一方面對于更好地理解框架實現原理也很有幫助。
小結
本講介紹了分布式系統遠程過程調用部分的第一個技術組件,即網絡通信。可以說網絡通信是一切分布式系統的基礎,我們從網絡連接、IO 模型等角度討論了與網絡通信相關的一些基本概念。
結尾歌
我喚醒大海
喚醒山脈
我喚醒沙漠
處處充滿色彩
美麗的地方
開心往前飛
就算有億萬公里
一噸行李
我們不放棄
前進需要勇氣
一直往前飛
最重要開心就好
忘記煩惱宇宙很大
任飛翔
滿載歡樂
回航
闖一闖
讓我們闖一闖
我們志氣要比天還高
云啊
輕輕飄過來
夢中輪廓一點點透露出來
飛吧飛吧
飛過黎明和夜晚
啦啦啦啦
風啊
輕輕吹過來
夢想翅膀流星天空中劃過
穿越時空
回到那夢想的地方
總結
以上是生活随笔為你收集整理的深造分布式 打败面试官 招式三 直捣黄龙的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机未来设计建筑,未来设计的趋势解析,
- 下一篇: Android实现可编辑下拉菜单