Socket 面對(duì)的挑戰(zhàn)?
在軟件中最普遍和生命力最強(qiáng)的接口之一就是是Socket API。Socket API最早是由由加州大學(xué)伯克利分校計(jì)算機(jī)系統(tǒng)研究小組開發(fā)的,在1982年作為 BSD 4.1c操作系統(tǒng)的一部分首次發(fā)布。雖然有一些使用時(shí)間更長的 API ,例如那些處理 Unix 文件 I/O 的 API ,但是一個(gè) API 能夠保持使用并且近40年來基本上沒有變化,這是及其令人印象深刻的事情了。對(duì)Socket API 的主要更新是擴(kuò)展了輔助程序,以適應(yīng) IPv6的大地址空間。
互聯(lián)網(wǎng)和整個(gè)網(wǎng)絡(luò)世界自 socket API 誕生以來已經(jīng)發(fā)生了非常重大的變化, API 已經(jīng)改變了開發(fā)者思考和編寫網(wǎng)絡(luò)應(yīng)用程序的方式,但是,網(wǎng)絡(luò)世界和各種服務(wù)的不斷變化,給socket API 帶來了哪些挑戰(zhàn)呢?
Socket 的歷史
1982年到如今,關(guān)于網(wǎng)絡(luò)的兩個(gè)最大區(qū)別是拓?fù)浜退俣?。人們注意到的是速度的提高,而不是拓?fù)浣Y(jié)構(gòu)的變化。在1982年,商用長途網(wǎng)絡(luò)鏈路的最大帶寬是1.5 Mbps。而所部署的以太局域網(wǎng)速度為10mbps。局域網(wǎng)上兩臺(tái)計(jì)算機(jī)之間的往返時(shí)間以幾十毫秒計(jì)算,互聯(lián)網(wǎng)上各系統(tǒng)之間的往返時(shí)間以幾百毫秒計(jì)算,這當(dāng)然取決于位置和一個(gè)數(shù)據(jù)包在計(jì)算機(jī)之間傳送時(shí)的跳數(shù)。一個(gè)家庭用戶能夠通過電話線連接到任何計(jì)算設(shè)備都是幸福的事,1995年,自己在當(dāng)時(shí)的電報(bào)局申請(qǐng)了BTA的郵箱,并興奮了很久。
當(dāng)時(shí)的網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)相對(duì)簡單,大多數(shù)計(jì)算機(jī)只連接到一個(gè)局域網(wǎng); 局域網(wǎng)連接到一個(gè)原始路由器,這個(gè)路由器可能有一些到其他局域網(wǎng)的連接或者到互聯(lián)網(wǎng)的一個(gè)連接。對(duì)于一個(gè)應(yīng)用程序到另一個(gè)應(yīng)用程序,連接要么跨越局域網(wǎng),要么傳輸一個(gè)或多個(gè)路由器。
分布式編程的模型中最普及的是基于socket API 的客戶端/服務(wù)器模型,其中有一個(gè)服務(wù)器和一組客戶端。客戶端向服務(wù)器發(fā)送消息,要求服務(wù)器代表它們完成工作,等待服務(wù)器完成請(qǐng)求的工作,然后在稍后的某個(gè)時(shí)刻收到答復(fù)。這種計(jì)算模型已經(jīng)無處不在,它通常是許多軟件工程師所熟悉的唯一模型。然而,在設(shè)計(jì)socket的時(shí)候,它被看作是在計(jì)算機(jī)網(wǎng)絡(luò)上擴(kuò)展 Unix 文件 I/O 模型的一種方法。另一個(gè)原因是它支持最流行的 TCP協(xié)議,本質(zhì)上具有點(diǎn)對(duì)點(diǎn)的通信模型。
Socket API 使客戶機(jī)/服務(wù)器模型易于實(shí)現(xiàn),程序員只需要將少量的系統(tǒng)調(diào)用添加到非聯(lián)網(wǎng)代碼中,就可以利用其他計(jì)算資源,這使得Socket API的客戶機(jī)/服務(wù)器模式已經(jīng)成為主導(dǎo)網(wǎng)絡(luò)計(jì)算的模式。
Socket 中的以下五個(gè)函數(shù)是 API 的核心,并且是與常規(guī)文件 I/O 的區(qū)別所在:
socket() | 創(chuàng)建通信端點(diǎn) |
---|---|
bind() | 將端點(diǎn)綁定到一組網(wǎng)絡(luò)層參數(shù) |
connect() | 連接服務(wù)器提交請(qǐng)求 |
listen() | 監(jiān)聽鏈路并設(shè)置請(qǐng)求的數(shù)量限制 |
accept() | 接受來自客戶端的一個(gè)或多個(gè)請(qǐng)求 |
實(shí)際上,socket ()調(diào)用可以替換為 open ()的一個(gè)變體,但是當(dāng)時(shí)還沒有這樣做。Socket ()和 open ()實(shí)際上都是將相同的東西返回給程序: 一個(gè)進(jìn)程唯一的文件描述符,并用于用于該 API 的所有后續(xù)操作。socket API 的簡單性導(dǎo)致了它的無處不在,但無處不在阻礙了替代或增強(qiáng)API 的開發(fā),而那些 API 可以幫助程序員開發(fā)其他類型的分布式應(yīng)用程序。
socket 面臨的挑戰(zhàn)
客戶機(jī)/服務(wù)器的計(jì)算模式在開發(fā)時(shí)具有許多優(yōu)點(diǎn)。它允許許多用戶共享資源,有了這種共享模式,就有可能提高資源的利用率。
然而,Socket AP在以下三個(gè)不同的網(wǎng)絡(luò)區(qū)域表現(xiàn)不佳:
- 低延遲或?qū)崟r(shí)應(yīng)用程序
- 高帶寬應(yīng)用程序
- 多宿主系統(tǒng)(即具有多個(gè)網(wǎng)絡(luò)接口的系統(tǒng))。
許多人混淆了增加網(wǎng)絡(luò)帶寬和提高性能,因?yàn)樵黾訋挷⒉灰欢〞?huì)減少延遲。Socket API 面臨的主要是性能挑戰(zhàn),即如何讓應(yīng)用程序更快地訪問網(wǎng)絡(luò)數(shù)據(jù)。
任何使用socket API 的程序發(fā)送和接收數(shù)據(jù)的方式都是通過對(duì)操作系統(tǒng)的調(diào)用。所有這些調(diào)用都有一個(gè)共同點(diǎn): 調(diào)用程序必須不斷地請(qǐng)求要傳遞的數(shù)據(jù),因?yàn)榉?wù)器不能在沒有客戶機(jī)請(qǐng)求的情況下做任何事情。然而,如果服務(wù)是音樂或視頻呢,那該怎么辦?在媒體分發(fā)服務(wù)中,可能有一個(gè)或多個(gè)數(shù)據(jù)源和多個(gè)監(jiān)聽器。只要用戶在收聽或查看媒體,最有可能的情況是應(yīng)用程序需要任何已經(jīng)到達(dá)的數(shù)據(jù)。不斷地請(qǐng)求新數(shù)據(jù)是對(duì)應(yīng)用程序的時(shí)間和資源的浪費(fèi)。Socket API 沒有向程序員提供這樣一種方式: “無論何時(shí)有數(shù)據(jù)需要處理,都直接調(diào)用socket來處理它。”
Socket 程序是從數(shù)據(jù)缺乏而不是數(shù)據(jù)豐富的角度編寫的。網(wǎng)絡(luò)程序非常習(xí)慣于等待數(shù)據(jù),因此使用一個(gè)單獨(dú)的系統(tǒng)調(diào)用例如 select () ,這樣就可以偵聽多個(gè)數(shù)據(jù)源,而不會(huì)阻塞單個(gè)請(qǐng)求?;?socket 的程序的典型處理循環(huán)不是簡單地 read ()、 process ()、 read () ,而是 select ()、 read ()、 process ()、 select ()。雖然將單個(gè)系統(tǒng)調(diào)用添加到循環(huán)中似乎不會(huì)增加太多負(fù)擔(dān),但情況并非如此。每個(gè)系統(tǒng)調(diào)用都需要將參數(shù)封送并復(fù)制到內(nèi)核中,同時(shí)導(dǎo)致系統(tǒng)阻塞調(diào)用進(jìn)程并調(diào)度另一個(gè)進(jìn)程。如果調(diào)用者在調(diào)用 select ()時(shí)可以獲得數(shù)據(jù),那么跨越用戶/內(nèi)核邊界的所有工作都將被浪費(fèi),因?yàn)?read ()會(huì)立即返回?cái)?shù)據(jù)。除非連續(xù)請(qǐng)求之間的間隔時(shí)間相當(dāng)長,否則常規(guī)的檢查/讀取/檢查是一種浪費(fèi)。
要克服這個(gè)問題,需要反轉(zhuǎn)應(yīng)用程序和操作系統(tǒng)之間的通信模型,提供一個(gè)允許內(nèi)核直接調(diào)用程序的 API 。但各種嘗試中沒有一個(gè)獲得廣泛接受。在開發(fā)sockket API 時(shí)存在的操作系統(tǒng),在一般情況下,都是在單處理器計(jì)算機(jī)上執(zhí)行單線程的。如果內(nèi)核反調(diào) API,就會(huì)有調(diào)用在哪個(gè)上下文中執(zhí)行的問題。這種軟件架構(gòu)唯一流行的地方是沒有用戶和虛擬內(nèi)存的嵌入式系統(tǒng)和網(wǎng)絡(luò)路由器。
虛擬內(nèi)存的問題使得實(shí)現(xiàn)內(nèi)核上行調(diào)用機(jī)制的問題更加復(fù)雜。分配給用戶進(jìn)程的內(nèi)存是虛擬內(nèi)存,但網(wǎng)絡(luò)接口等設(shè)備使用的內(nèi)存是物理內(nèi)存。讓內(nèi)核將物理內(nèi)存從設(shè)備映射到用戶空間,打破了虛擬內(nèi)存系統(tǒng)提供的基本保護(hù)。
面對(duì)挑戰(zhàn)的嘗試與猜想
為了克服socket API 中存在的性能問題,有幾種不同的機(jī)制,有時(shí)在不同的操作系統(tǒng)上實(shí)現(xiàn)了這些機(jī)制。
低延遲的網(wǎng)絡(luò)應(yīng)用
對(duì)于那些更關(guān)心延遲的程序而言,所做的工作很少。對(duì)于正在等待網(wǎng)絡(luò)事件的程序來說,唯一重要的改進(jìn)是添加了一組程序可以等待的內(nèi)核事件,實(shí)現(xiàn)異步通知機(jī)制。例如 kevents () 是 select ()機(jī)制的擴(kuò)展,它包含了內(nèi)核可能告訴程序的任何可能的事件。在 kevents ()出現(xiàn)之前,用戶程序可以在任何文件描述符上調(diào)用 select () ,這樣程序就可以知道一組文件描述符中的任何一個(gè)是可讀的、可寫的,或者有錯(cuò)誤。當(dāng)程序被寫入一個(gè)循環(huán)并等待一組文件描述符時(shí),例如從網(wǎng)絡(luò)讀取并寫入磁盤ー select ()調(diào)用就足夠了,但是一旦程序想檢查其他事件,例如計(jì)時(shí)器和信號(hào),select ()就無能為力了。低延遲應(yīng)用程序的問題在于 kevents ()不傳遞數(shù)據(jù),只傳遞數(shù)據(jù)就緒的信號(hào)。下一個(gè)邏輯步驟是使用基于事件的 API 來傳遞數(shù)據(jù)。為了獲得內(nèi)核知道應(yīng)用程序需要的數(shù)據(jù),讓應(yīng)用程序兩次跨越用戶/內(nèi)核邊界是沒有道理的。
高帶寬的網(wǎng)絡(luò)應(yīng)用
因?yàn)閺?fù)制數(shù)據(jù)會(huì)降低網(wǎng)絡(luò)協(xié)議的性能,其中一種機(jī)制是零拷貝socket,為了提高對(duì)高帶寬更感興趣的網(wǎng)絡(luò)應(yīng)用程序速度,對(duì)操作系統(tǒng)進(jìn)行了修改,以避免更多的數(shù)據(jù)副本。
傳統(tǒng)上,操作系統(tǒng)對(duì)系統(tǒng)接收到的每個(gè)數(shù)據(jù)包執(zhí)行兩個(gè)副本。第一個(gè)拷貝由網(wǎng)絡(luò)驅(qū)動(dòng)程序從網(wǎng)絡(luò)設(shè)備的內(nèi)存中執(zhí)行到內(nèi)核的內(nèi)存中,第二個(gè)拷貝由內(nèi)核中的socket層在用戶程序讀取數(shù)據(jù)時(shí)執(zhí)行。系統(tǒng)接收到的每個(gè)消息都要執(zhí)行拷貝,導(dǎo)致這些復(fù)制操作的成本都較高。同理,當(dāng)程序想要發(fā)送一條消息時(shí),必須將發(fā)送的每條消息的數(shù)據(jù)從用戶程序復(fù)制到內(nèi)核; 然后再被復(fù)制到設(shè)備用來在網(wǎng)絡(luò)上傳輸?shù)木彌_區(qū)中。
數(shù)據(jù)復(fù)制對(duì)系統(tǒng)性能是一種詛咒,可以努力在內(nèi)核中最小化這種復(fù)制。內(nèi)核避免數(shù)據(jù)拷貝的最簡單方法是讓設(shè)備驅(qū)動(dòng)程序?qū)?shù)據(jù)直接復(fù)制到內(nèi)核內(nèi)存中或從內(nèi)核內(nèi)存中復(fù)制出來。在現(xiàn)代網(wǎng)絡(luò)的設(shè)備上,這是如何構(gòu)建內(nèi)存的結(jié)果。驅(qū)動(dòng)程序和內(nèi)核共享兩個(gè)分組描述符環(huán)(一個(gè)用于發(fā)送,一個(gè)用于接收) ,其中每個(gè)描述符都有一個(gè)指向內(nèi)存的指針。網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序最初用內(nèi)核的內(nèi)存填充這些發(fā)送/接收環(huán)。當(dāng)接收到數(shù)據(jù)時(shí),設(shè)備在正確的接收描述符中設(shè)置一個(gè)標(biāo)志,通常通過中斷告訴內(nèi)核有數(shù)據(jù)等待。然后,內(nèi)核從接收描述符環(huán)中刪除已填充的緩沖區(qū),并將其替換為新的緩沖區(qū),以便設(shè)備填充。數(shù)據(jù)包以緩沖區(qū)的形式在網(wǎng)絡(luò)堆棧中移動(dòng),直到到達(dá)套接字層,當(dāng)用戶的程序調(diào)用 read ()時(shí),數(shù)據(jù)包從內(nèi)核中復(fù)制出來。程序發(fā)送的數(shù)據(jù)由內(nèi)核以類似的方式處理,內(nèi)核緩沖區(qū)最終被添加到傳輸描述符環(huán)中,然后設(shè)置一個(gè)標(biāo)志來告訴設(shè)備它可以將數(shù)據(jù)放在網(wǎng)絡(luò)上的緩沖區(qū)中。
內(nèi)核中的所有這些工作都沒有解決最后那個(gè)拷貝的問題,仍然是跨用戶/內(nèi)核邊界安全地共享內(nèi)存。內(nèi)核無法將其內(nèi)存提供給用戶程序,因?yàn)檫@時(shí)它將失去對(duì)內(nèi)存的控制。崩潰的用戶程序可能會(huì)使內(nèi)核失去大量可用內(nèi)存,從而導(dǎo)致系統(tǒng)性能下降??鐑?nèi)核/用戶邊界共享內(nèi)存緩沖區(qū)也存在固有的安全問題。此時(shí),對(duì)于如何使用 sockets API 實(shí)現(xiàn)更高的帶寬,暫時(shí)沒有單一的答案。
多宿主的網(wǎng)絡(luò)應(yīng)用
socket API 不僅在應(yīng)用程序編寫上存在性能問題,而且還減少了可能發(fā)生的通信類型。客戶機(jī)/服務(wù)器模式本質(zhì)上是點(diǎn)對(duì)點(diǎn)的通信類型。雖然服務(wù)器可以處理來自不同客戶機(jī)組的請(qǐng)求,但是每個(gè)客戶機(jī)對(duì)于一個(gè)請(qǐng)求或一組請(qǐng)求只有一個(gè)到單個(gè)服務(wù)器的連接。在一個(gè)每臺(tái)計(jì)算機(jī)只有一個(gè)網(wǎng)絡(luò)接口的世界里,這種模式非常合理。客戶機(jī)和服務(wù)器之間的連接由 < 源 IP,源端口,目標(biāo) IP,目標(biāo)端口 > 來標(biāo)識(shí)。由于服務(wù)通常有一個(gè)眾所周知的目標(biāo)端口(例如,HTTP 的目標(biāo)端口為80) , IP 地址是固定的,所以唯一可以容易改變的值是源端口。
在socket API誕生的年代,每臺(tái)不是路由器的計(jì)算機(jī)只有一個(gè)網(wǎng)絡(luò)接口,這意味著為了識(shí)別一個(gè)服務(wù),客戶端計(jì)算機(jī)需要一個(gè)目的地址和端口,而它本身只有一個(gè)源地址和端口。一臺(tái)計(jì)算機(jī)用多種方式獲得服務(wù)的想法過于復(fù)雜,而且實(shí)現(xiàn)起來成本太高??紤]到這些限制,sockets API 沒有理由向程序員展示編寫多宿主程序的能力,這樣的呈現(xiàn)可以管理對(duì)其重要的接口或連接。這些特性在實(shí)現(xiàn)時(shí)是操作系統(tǒng)中路由軟件的一部分。程序最終能夠訪問它們的唯一途徑是通過一組名為路由套接字(routing socket)的非標(biāo)準(zhǔn)內(nèi)核 API。
在具有多個(gè)網(wǎng)絡(luò)接口的系統(tǒng)上,使用標(biāo)準(zhǔn)的Socket API 編寫一個(gè)可以輕松地多網(wǎng)址址的應(yīng)用程序是不可能的。如果那樣的話,在利用這兩個(gè)接口時(shí),如果其中一個(gè)出現(xiàn)故障,或者如果數(shù)據(jù)包流經(jīng)的主要路由出現(xiàn)故障,應(yīng)用程序不會(huì)失去與服務(wù)器的連接。
盡管SCTP 在協(xié)議級(jí)別集成了對(duì)多宿主的支持,但是不可能通過socket API 導(dǎo)出這種支持。最初提供了幾個(gè)臨時(shí)系統(tǒng)調(diào)用,這是訪問這一功能的唯一方法。到目前為止,這可能是唯一一個(gè)同時(shí)具有這個(gè)特性的能力和用戶需求的協(xié)議,但這個(gè) API 還沒有在多個(gè)操作系統(tǒng)中標(biāo)準(zhǔn)化。下表列出了 SCTP 添加的API:
sctp_bindx() | 將 SCTP socket綁定或取消綁定到地址列表 |
---|---|
sctp_connectx() | 使用多個(gè)目標(biāo)地址連接 SCTP socket |
sctp_generic_recvmsg() | 從對(duì)等點(diǎn)接收數(shù)據(jù) |
sctp_generic_sendmsg() | 將數(shù)據(jù)發(fā)往對(duì)等點(diǎn) |
sctp_getaddrlen() | 返回地址族的地址長度 |
sctp_getassocid() | 返回指定socket地址的關(guān)聯(lián) ID |
ctp_getpaddrs()< | 將地址列表返回給調(diào)用者 |
sctp_peeloff() | 將關(guān)聯(lián)從一對(duì)多套接字分離到單獨(dú)的文件描述符 |
ctp_getpaddrs() | 將地址列表返回給調(diào)用者 |
sctp_sendx() | 從 SCTP 套接字發(fā)送消息 |
sctp_sendmsgx() | 從 SCTP 套接字發(fā)送消息 |
雖然這個(gè)函數(shù)列表超過了API必需的數(shù)量,但需要注意的是,許多函數(shù)都是socket api 的衍生品,例如 send () ,需要擴(kuò)展才能在一個(gè)多宿主的世界中工作?,F(xiàn)在的問題是Socket API無處不在,以至于很難改變現(xiàn)有的 API 集合,害怕混淆用戶或者已有的應(yīng)用程序。
隨著系統(tǒng)內(nèi)置了越來越多的網(wǎng)絡(luò)接口,編寫利用多宿主應(yīng)用程序的能力將是必要的。很容易地想象這種技術(shù)在智能手機(jī)中的應(yīng)用,智能手機(jī)有三個(gè)顯然的網(wǎng)絡(luò)接口: 通過蜂窩網(wǎng)絡(luò)的接口,WiFi 接口,通常還有一個(gè)藍(lán)牙接口。如果哪怕只有一個(gè)網(wǎng)絡(luò)接口正常工作,應(yīng)用程序也不應(yīng)該失去連接性。應(yīng)用程序設(shè)計(jì)者的問題在于,希望自己的代碼能夠在很少或沒有任何變化的情況下,通過大量的設(shè)備工作,從手機(jī)到筆記本電腦,再到臺(tái)式機(jī)等等。有了正確定義的API,就可以移除阻止這種情況發(fā)生。只是由于 socket API “足夠好”的事實(shí),這種需求尚未得到滿足。
小結(jié)
對(duì)高帶寬、低延遲和多宿主的支持是socket API 需要面對(duì)的挑戰(zhàn)。局域網(wǎng)現(xiàn)在已經(jīng)達(dá)到10 Gbps,對(duì)于許多應(yīng)用程序來說,客戶機(jī)/服務(wù)器風(fēng)格的通信效率太低,可能無法高效使用可用的帶寬。擴(kuò)展socket API 支持的通信范例,以允許跨內(nèi)核邊界共享內(nèi)存,允許將數(shù)據(jù)傳送到應(yīng)用程序的低延遲機(jī)制。另外,因?yàn)榫哂卸鄠€(gè)主動(dòng)接口的設(shè)備正在成為網(wǎng)絡(luò)系統(tǒng)的標(biāo)準(zhǔn),多宿主的支持也應(yīng)該成為socket API 的一個(gè)特性。