從OkHTTP請(qǐng)求到讀取回復(fù)都發(fā)生了什么

記得很久看到過(guò)一篇博客《從輸入網(wǎng)址到網(wǎng)頁(yè)出現(xiàn)在瀏覽器中都發(fā)生了什么》(名字太長(zhǎng)星爪,現(xiàn)在已經(jīng)記不得了T T),佩服于作者深厚的技術(shù)積淀,能夠從上到下將整個(gè)過(guò)程梳理的一清二楚。

從接觸Android開(kāi)發(fā)以來(lái),我接觸了Volley和OkHttp等優(yōu)秀的網(wǎng)絡(luò)庫(kù)庶香,加上之前對(duì)Linux也有所接觸,就心血來(lái)潮简识,想要仿照他人寫一篇從上到下梳理Android下網(wǎng)絡(luò)報(bào)文發(fā)送邏輯的文章赶掖。我雖功力未到如此境界,但也想盡力梳理一遍我所知道的部分七扰。

一句話總結(jié):
我們從OkHttp開(kāi)始奢赂,通過(guò)調(diào)用JAVA的方法進(jìn)行DNS解析,獲取ip地址颈走。然后通過(guò)OkHttp的連接池建立TCP三次握手膳灶,之后進(jìn)行高效的數(shù)據(jù)傳送,最后讀出回復(fù)內(nèi)容包文立由。

由于Socket背后代表的TCP/IP協(xié)議棧與Linux下的VFS無(wú)縫對(duì)接轧钓,所以我們對(duì)Socket返回的文件描述的讀寫都會(huì)進(jìn)入Linux TCP/IP協(xié)議棧中,并發(fā)送給遠(yuǎn)程服務(wù)器锐膜。

首先毕箍,我們從這一段代碼開(kāi)始這次旅程。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    sendRequest();
}

private void sendRequest() {
    String url = "http://www.reibang.com";
    OkHttpClient okHttpClient = new OkHttpClient();
    Request request = new Request.Builder()
            .url(url)
            .build();
    final Call call = okHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {}

        @Override
        public void onResponse(Call call, Response response) throws IOException {
           Log.d(TAG, response.body().string());
        });
    }
}

這是一段非常簡(jiǎn)單的OkHttp發(fā)送網(wǎng)絡(luò)請(qǐng)求的代碼道盏,用來(lái)訪問(wèn)特定網(wǎng)頁(yè)而柑,若訪問(wèn)能夠成功文捶,則會(huì)在日志中將網(wǎng)頁(yè)內(nèi)容打印出來(lái)。當(dāng)我們打開(kāi)了這個(gè)APP牺堰,到日志中出現(xiàn)了網(wǎng)頁(yè)內(nèi)容拄轻,要經(jīng)過(guò)好幾個(gè)車站哩,大概有這樣幾站伟葫。

[網(wǎng)絡(luò)框架]-->[Framework]-->[Native]-->[Linux]

由于我們使用了OkHttp框架,所以會(huì)有網(wǎng)絡(luò)框架這一站院促,當(dāng)然你也可以直接使用Java的Socket接口進(jìn)行編程筏养。無(wú)論采用哪一種方式,第二站就會(huì)進(jìn)入到FrameWork站常拓,利用Java接口實(shí)現(xiàn)網(wǎng)絡(luò)功能渐溶,但這一站也不是直接和操作系統(tǒng)打交道,通過(guò)虛擬機(jī)Android Runtime提供的接口進(jìn)入第三站Native層弄抬,這一層更靠近操作系統(tǒng)茎辐,并且最終調(diào)用操作系統(tǒng)提供的Libc庫(kù)與操作系統(tǒng)打交道,即進(jìn)入了第四站掂恕。接下去的事情就是操作系統(tǒng)使用Tcp/Ip協(xié)議棧發(fā)送報(bào)文和接收?qǐng)?bào)文的過(guò)程了拖陆。

代碼首先創(chuàng)建一個(gè)OkHttpClient和一個(gè)網(wǎng)絡(luò)請(qǐng)求類Request,然后再將Request放置于OkHttp工作隊(duì)列實(shí)現(xiàn)異步執(zhí)行懊亡。本篇文章就從call.enqueue()開(kāi)始分析依啰。

從隊(duì)列中取出Request

OkHttp允許同步執(zhí)行請(qǐng)求和異步執(zhí)行請(qǐng)求,本文分析異步執(zhí)行請(qǐng)求的過(guò)程店枣。在異步請(qǐng)求的模式下速警,OkHttp內(nèi)部維護(hù)了線程池,這個(gè)線程池沒(méi)有核心線程鸯两,不設(shè)置非核心線程數(shù)目上限闷旧,這表明一旦有Request進(jìn)入隊(duì)列會(huì)馬上被線程池處理。異步模式下使用2個(gè)的隊(duì)列钧唐。一個(gè)是異步執(zhí)行隊(duì)列忙灼,一個(gè)是準(zhǔn)備隊(duì)列。 但是okhttp默認(rèn)限制了同時(shí)處理request的上限為64個(gè)逾柿,當(dāng)超過(guò)64個(gè)時(shí)就暫時(shí)放到ready隊(duì)列缀棍,執(zhí)行完一個(gè)request之后再把request從ready放到running隊(duì)列。

Http協(xié)議對(duì)于同一個(gè)客戶端同一域名的并發(fā)量限制為2個(gè)机错,但是目前的客戶端基本無(wú)視這一規(guī)定爬范,chrome瀏覽器一般對(duì)同一域名的并行tcp連接是6個(gè),OkHttp規(guī)定為5個(gè)弱匪,如果超過(guò)也是先放入ready隊(duì)列青瀑。也就是說(shuō)璧亮,如果我們連續(xù)向www.reibang.com發(fā)出了6個(gè)Request,那么第六個(gè)Request會(huì)首先進(jìn)入準(zhǔn)備隊(duì)列斥难,當(dāng)前5個(gè)Request有一個(gè)收到了回復(fù)之后枝嘶,才可以發(fā)送這一個(gè)Request。

建立連接


第一站OkHttp

好了哑诊,OkHttp取出了"www.reibang.com"的Request群扶,開(kāi)始建立連接。眾所周知镀裤,Http/1.1就默認(rèn)開(kāi)啟keep-alive選項(xiàng)竞阐,因此,OkHttp會(huì)首先從ConnectionPool(連接池)中尋找是否有建立好的與"www.reibang.com"的連接暑劝,如果可以找到的話就直接上車吧~如果沒(méi)有的話骆莹,就只能新建一個(gè)連接了。

OkHttp將Ip,Hostname和Proxy等信息封裝成Route結(jié)構(gòu)担猛,并且用Set維護(hù)一個(gè)RouteDataBase幕垦,用來(lái)保存哪些地址是成功訪問(wèn)的,哪些是失敗訪問(wèn)傅联。當(dāng)新建連接時(shí)先改,OkHttp首先從RouteDataBase中查找所需的信息。//TODO 添加代理部分 http://www.reibang.com/p/5c98999bc34f
獲取滿足條件的地址纺且,最后調(diào)用RealConnection的connct()方法建立連接盏道。成功建立之后將其置于連接池中,并且在RouteDataBase中添加Route载碌。

第二站Framework

[SocksSocketImpl::connect]-->[AbstractPlainSocketImpl::connect]
-->[PlainSocketImpl::socketConnect]

SocksSocketImpl是PlainSockImpl的子類猜嘱,PlainSockImpl又是AbstractPlainSockImpl的子類。
SocketSocketImpl只是簡(jiǎn)單的調(diào)用父類的connect方法嫁艇,AbstractPlainSockImpl::connect為Socket添加Ip與port朗伶,然后調(diào)用子類的socketConnect方法,并進(jìn)入了Native層步咪。

第三站Native

創(chuàng)建socket:進(jìn)入/libcore/ojluni/src/main/native/PlainSocketImpl.c中的PlainSocketImpl_socketCreate根據(jù)Java層傳入的參數(shù)判斷TCP或者UDP论皆。fd = JVM_Socket(domain, type, 0))創(chuàng)建socket,并在此函數(shù)體內(nèi)進(jìn)入第四站猾漫。創(chuàng)建成功之后点晴,將fd設(shè)置到JAVA層的響應(yīng)參數(shù)中。

connect:進(jìn)入/libcore/ojluni/src/main/native/PlainSocketImpl.c中的PlainSocketImpl_socketConnect
首先從JAVA層中獲取必要的信息悯周,然后調(diào)用NET_InetAddressToSockaddr設(shè)置struct sockaddr由于我們?cè)O(shè)置了timeout=0粒督,所以Native設(shè)置了非阻塞模式創(chuàng)建socket連接fcntl(fd, F_SETFL, flags|O_NON_BLOCK)然后調(diào)用connect()函數(shù),這個(gè)函數(shù)就是由Linux提供的LibC庫(kù)中的函數(shù)了禽翼。由于我們?cè)O(shè)置了非阻塞模式屠橄,所以此函數(shù)立馬返回族跛,那么如何知道連接的結(jié)果呢?這里锐墙,Native使用了I/O復(fù)用函數(shù)礁哄,可以選擇使用POLL,也可以選擇使用Select溪北,由編譯選項(xiàng)決定桐绒。Select函數(shù)和Poll函數(shù)的工作原理類似,都是以輪詢的方式查詢文件描述符是否準(zhǔn)備就緒之拨,一旦fd準(zhǔn)備就緒就返回掏膏,然后我們就可以讀取fd中的數(shù)據(jù)。唯一不同的是select只支持read敦锌,write,exception三個(gè)事件而poll函數(shù)可以定義多個(gè)事件佳簸,比select更加的“聰明”乙墙。

如果我們監(jiān)聽(tīng)的fd(即socket fd)有寫數(shù)據(jù),則我們獲得了一個(gè)本地端口生均,將其端口通過(guò)反射在Java類中設(shè)置听想。

第四站Linux

第三站調(diào)用創(chuàng)建socket和connect()函數(shù)后就進(jìn)入了第四站。

[Socket]-->[VFS]-->[Sockfs]-->[TCP/IP]

進(jìn)入socket_create():

  1. 安全性檢查

  2. 對(duì)于type和family進(jìn)行判斷和重新賦值

  3. 根據(jù)不同的協(xié)議調(diào)用不同的create函數(shù)马胧,返回sock結(jié)構(gòu)體汉买。這里就調(diào)用inet_create函數(shù)。
    sock結(jié)構(gòu)體保存了大量的信息佩脊,定義了TCP傳輸中的大量參數(shù)蛙粘,包括發(fā)送緩沖區(qū),接收緩沖區(qū)威彰,計(jì)時(shí)器出牧,標(biāo)志位,狀態(tài)歇盼,引用計(jì)數(shù)等等舔痕。

  4. 創(chuàng)建inode,inode是Linux文件系統(tǒng)中的節(jié)點(diǎn)豹缀。

  5. 獲得本進(jìn)程的一個(gè)空閑的文件描述符伯复,申請(qǐng)一個(gè)新的目錄項(xiàng),將文件操作的函數(shù)指針設(shè)置到inode節(jié)點(diǎn)中

先初始化路徑path:其目錄項(xiàng)的父目錄項(xiàng)為超級(jí)塊對(duì)應(yīng)的根目錄邢笙,名稱為空啸如,操作對(duì)象為sockfs_dentry_operations,對(duì)應(yīng)的索引節(jié)點(diǎn)對(duì)象為sock套接字關(guān)聯(lián)的索引節(jié)點(diǎn)對(duì)象鸣剪,即SOCK_INODE(sock)组底;裝載點(diǎn)為sock_mnt丈积。

申請(qǐng)一個(gè)此路徑下的file,sock->file = file; file->private_data = sock;file和sock雙向綁定债鸡。最后返回fd即我們創(chuàng)建的Socket的fd江滨。
這樣就將socket與文件系統(tǒng)綁定了,因此我們可以對(duì)socket進(jìn)行文件操作厌均。

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);

如果連接建立成功則返回0唬滑,否則連接建立失敗。在connect函數(shù)中進(jìn)行TCP三次握手棺弊,建立TCP連接晶密。

讀寫數(shù)據(jù)


第一站OkHttp

OkHttp支持Chunk方式寫入數(shù)據(jù),這種方式類似于Ip的分包模她,將數(shù)據(jù)分包傳送稻艰,當(dāng)客戶端接收到一個(gè)CHunk就可以立馬進(jìn)行處理,而不必等到所有數(shù)據(jù)都接收完畢才可以處理侈净。好吧尊勿,我們暫時(shí)不分析這個(gè)過(guò)程。最簡(jiǎn)單的畜侦,OkHttp使用了Okio進(jìn)行io元扔。
Okio中提供了讀數(shù)據(jù)類型Source和寫數(shù)據(jù)類型Sink,分別對(duì)應(yīng)原生的InputStream和OutputStream旋膳。Okio提供了常用數(shù)據(jù)讀寫的處理類澎语,簡(jiǎn)化了讀寫操作,并且增加了緩沖區(qū)管理验懊,能夠更加高效的管理和使用內(nèi)存擅羞。

整個(gè)包裹流:
RealBufferedSink(newFixedLengthSink(RealBufferedSink(AsyncTimeout::sink(Sink(socket.outputStream)))))

Okio是高效的io框架,使用Segment結(jié)構(gòu)體保存數(shù)據(jù)鲁森,并且使用緩沖池來(lái)緩存Segment減少GC祟滴。同時(shí),一個(gè)Segment存儲(chǔ)使用率大于50%歌溉,以保證內(nèi)存使用效率垄懂。

第二站Framework

封裝了這么多層,最后調(diào)用SocketOutputStream進(jìn)行寫數(shù)據(jù)操作痛垛。在SocketOutputStream中最終調(diào)用private native void writeba_native(byte[] b, int off, int len, FileDescriptor fd) throws IOException;進(jìn)入Native

第三站Native

進(jìn)入Native草慧,首先獲取socket描述符,獲取數(shù)據(jù)所在的數(shù)組匙头,再調(diào)用socket_write_all漫谷,在這個(gè)函數(shù)中,使用了sendmsg函數(shù)進(jìn)行蹂析。

第四站Linux

由于socketfd_file_ops沒(méi)有定義read操作舔示,所以進(jìn)入LinuxVFS層后碟婆,行為被轉(zhuǎn)發(fā)到vfs_read,并進(jìn)入do_sync_read.
sendmsg是通用的數(shù)據(jù)讀寫函數(shù)惕稻,可以用于跨進(jìn)程傳輸數(shù)據(jù)并且可以在傳輸過(guò)程中使用命令控制傳輸過(guò)程竖共。socket在此處使用此方法,將數(shù)據(jù)寫入Linux內(nèi)核俺祠。

關(guān)閉連接


第一站OkHttp

由于OKHttp使用了連接池的概念公给,所以socket.close的動(dòng)作由連接池管理,當(dāng)5分鐘沒(méi)有通信后蜘渣,連接池就會(huì)將連接關(guān)閉淌铐。最終會(huì)調(diào)用socket.close()

第二站Framework

AbstractPlainSocketImpl()::close()這里使用了引用計(jì)數(shù),當(dāng)某socket不再被任何對(duì)象引用時(shí)蔫缸,才真正執(zhí)行close操作腿准。close()操作分兩步:

  1. 關(guān)閉socket,但是不釋放文件描述符
  2. 關(guān)閉并且釋放文件描述符

第三站Native

進(jìn)入Native層執(zhí)行socketClose0()
兩部釋放的具體實(shí)現(xiàn):

  1. 將socket和fd分開(kāi)untagSocket
  2. closefd

第四站Linux

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拾碌,一起剝皮案震驚了整個(gè)濱河市释涛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌倦沧,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件它匕,死亡現(xiàn)場(chǎng)離奇詭異展融,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)豫柬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門告希,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人烧给,你說(shuō)我怎么就攤上這事燕偶。” “怎么了础嫡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵指么,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我榴鼎,道長(zhǎng)伯诬,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任巫财,我火速辦了婚禮盗似,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘平项。我一直安慰自己赫舒,他們只是感情好悍及,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著接癌,像睡著了一般心赶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扔涧,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天园担,我揣著相機(jī)與錄音,去河邊找鬼枯夜。 笑死弯汰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的湖雹。 我是一名探鬼主播咏闪,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼摔吏!你這毒婦竟也來(lái)了鸽嫂?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤征讲,失蹤者是張志新(化名)和其女友劉穎据某,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體诗箍,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡癣籽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滤祖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筷狼。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖匠童,靈堂內(nèi)的尸體忽然破棺而出埂材,到底是詐尸還是另有隱情,我是刑警寧澤汤求,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布俏险,位于F島的核電站,受9級(jí)特大地震影響扬绪,放射性物質(zhì)發(fā)生泄漏寡喝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一勒奇、第九天 我趴在偏房一處隱蔽的房頂上張望预鬓。 院中可真熱鬧,春花似錦、人聲如沸格二。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)顶猜。三九已至沧奴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間长窄,已是汗流浹背滔吠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挠日,地道東北人疮绷。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嚣潜,于是被迫代替她去往敵國(guó)和親冬骚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1三個(gè)相關(guān)數(shù)據(jù)結(jié)構(gòu). 關(guān)于socket的創(chuàng)建懂算,首先需要分析socket這個(gè)結(jié)構(gòu)體只冻,這是整個(gè)的核心。 104 str...
    ice_camel閱讀 2,824評(píng)論 1 8
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,130評(píng)論 25 707
  • iPhone的標(biāo)準(zhǔn)推薦是CFNetwork 庫(kù)編程计技,其封裝好的開(kāi)源庫(kù)是 cocoa AsyncSocket庫(kù)喜德,用它...
    Ethan_Struggle閱讀 2,244評(píng)論 2 12
  • 最近在學(xué)習(xí)Python看了一篇文章寫得不錯(cuò),是在腳本之家里的垮媒,原文如下住诸,很有幫助: 一、網(wǎng)絡(luò)知識(shí)的一些介紹 soc...
    qtruip閱讀 2,708評(píng)論 0 6
  • 坐公交車的時(shí)候總喜歡看著窗外涣澡。 看著那些轉(zhuǎn)瞬即逝的光影。 頭微微發(fā)暈丧诺。 只是忽然入桂。 很想你。 用雙手遮住漫天飛揚(yáng)的...
    yzwjjx閱讀 162評(píng)論 0 0