記得很久看到過(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():
安全性檢查
對(duì)于type和family進(jìn)行判斷和重新賦值
根據(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ù)等等舔痕。創(chuàng)建inode,inode是Linux文件系統(tǒng)中的節(jié)點(diǎn)豹缀。
獲得本進(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()操作分兩步:
- 關(guān)閉socket,但是不釋放文件描述符
- 關(guān)閉并且釋放文件描述符
第三站Native
進(jìn)入Native層執(zhí)行socketClose0()
兩部釋放的具體實(shí)現(xiàn):
- 將socket和fd分開(kāi)
untagSocket
closefd