Socket簡(jiǎn)介
套接字socket是大多數(shù)程序員都非常熟悉的概念泪幌,它是計(jì)算機(jī)網(wǎng)絡(luò)編程的基礎(chǔ)肴焊,TCP/UDP收發(fā)消息都靠它其屏。我們熟悉的web服務(wù)器底層依賴它,我們用到的MySQL關(guān)系數(shù)據(jù)庫(kù)措译、Redis內(nèi)存數(shù)據(jù)庫(kù)底層依賴它别凤。我們用微信和別人聊天也依賴它,我們玩網(wǎng)絡(luò)游戲時(shí)依賴它领虹,讀者們能夠閱讀這篇文章也是因?yàn)橛兴诒澈竽刂С种W(wǎng)絡(luò)通信闻妓。
Socket傳輸?shù)幕具^(guò)程
當(dāng)客戶端和服務(wù)器使用TCP協(xié)議進(jìn)行通信時(shí),客戶端封裝一個(gè)請(qǐng)求對(duì)象req掠械,將請(qǐng)求對(duì)象req序列化成字節(jié)數(shù)組由缆,然后通過(guò)套接字socket將字節(jié)數(shù)組發(fā)送到服務(wù)器,服務(wù)器通過(guò)套接字socket讀取到字節(jié)數(shù)組猾蒂,再反序列化成請(qǐng)求對(duì)象req均唉,進(jìn)行處理,處理完畢后肚菠,生成一個(gè)響應(yīng)對(duì)應(yīng)res舔箭,將響應(yīng)對(duì)象res序列化成字節(jié)數(shù)組,然后通過(guò)套接字將自己數(shù)組發(fā)送給客戶端蚊逢,客戶端通過(guò)套接字socket讀取到自己數(shù)組层扶,再反序列化成響應(yīng)對(duì)象。
通信框架往往可以將序列化的過(guò)程隱藏起來(lái)烙荷,我們所看到的現(xiàn)象就是上圖所示镜会,請(qǐng)求對(duì)象req和響應(yīng)對(duì)象res在客戶端和服務(wù)器之間跑來(lái)跑去。也許你覺得這個(gè)過(guò)程還是挺簡(jiǎn)單的终抽,很好理解戳表,但是實(shí)際上背后發(fā)生的一系列事件超出了你們中大多數(shù)人的想象桶至。通信的真實(shí)過(guò)程要比上面的這張圖復(fù)雜太多。你也許會(huì)問(wèn)匾旭,我們需要了解的那么深入么镣屹,直接拿來(lái)用不就可以了么?在互聯(lián)網(wǎng)技術(shù)服務(wù)行業(yè)工作多年的經(jīng)驗(yàn)告訴我价涝,如果你對(duì)底層機(jī)制不了解女蜈,你就會(huì)不明白為什么對(duì)套接字socket的讀寫會(huì)出現(xiàn)各種奇奇怪怪的問(wèn)題,為什么有時(shí)會(huì)阻塞色瘩,有時(shí)又不阻塞鞭光,有時(shí)候還報(bào)錯(cuò),為什么會(huì)有粘包半包問(wèn)題泞遗,NIO具體又是什么,它是什么特別新鮮的技術(shù)么席覆?對(duì)于這些問(wèn)題的理解都需要你了解底層機(jī)制史辙。
Socket傳輸?shù)木唧w細(xì)節(jié)
我們平時(shí)用到的套接字其實(shí)只是一個(gè)引用(一個(gè)對(duì)象ID),這個(gè)套接字對(duì)象實(shí)際上是放在操作系統(tǒng)內(nèi)核中佩伤。這個(gè)套接字對(duì)象內(nèi)部有兩個(gè)重要的緩沖結(jié)構(gòu)聊倔,一個(gè)是讀緩沖(read buffer),一個(gè)是寫緩沖(write buffer)生巡,它們都是有限大小的數(shù)組結(jié)構(gòu)耙蔑。
當(dāng)我們對(duì)客戶端的socket寫入字節(jié)數(shù)組時(shí)(序列化后的請(qǐng)求消息對(duì)象req),是將字節(jié)數(shù)組拷貝到內(nèi)核區(qū)套接字對(duì)象的write buffer中孤荣,內(nèi)核網(wǎng)絡(luò)模塊會(huì)有單獨(dú)的線程負(fù)責(zé)不停地將write buffer的數(shù)據(jù)拷貝到網(wǎng)卡硬件甸陌,網(wǎng)卡硬件再將數(shù)據(jù)送到網(wǎng)線,經(jīng)過(guò)一些列路由器交換機(jī)盐股,最終送達(dá)服務(wù)器的網(wǎng)卡硬件中钱豁。
同樣,服務(wù)器內(nèi)核的網(wǎng)絡(luò)模塊也會(huì)有單獨(dú)的線程不停地將收到的數(shù)據(jù)拷貝到套接字的read buffer中等待用戶層來(lái)讀取疯汁。最終服務(wù)器的用戶進(jìn)程通過(guò)socket引用的read方法將read buffer中的數(shù)據(jù)拷貝到用戶程序內(nèi)存中進(jìn)行反序列化成請(qǐng)求對(duì)象進(jìn)行處理牲尺。然后服務(wù)器將處理后的響應(yīng)對(duì)象走一個(gè)相反的流程發(fā)送給客戶端,這里就不再具體描述幌蚊。
Socket發(fā)生阻塞的原因
我們注意到write buffer空間都是有限的谤碳,所以如果應(yīng)用程序往套接字里寫的太快,這個(gè)空間是會(huì)滿的溢豆。一旦滿了蜒简,寫操作就會(huì)阻塞,直到這個(gè)空間有足夠的位置騰出來(lái)漩仙。不過(guò)有了NIO(非阻塞IO)臭蚁,寫操作也可以不阻塞最铁,能寫多少是多少,通過(guò)返回值來(lái)確定到底寫進(jìn)去多少垮兑,那些沒有寫進(jìn)去的內(nèi)容用戶程序會(huì)緩存起來(lái)冷尉,后續(xù)會(huì)繼續(xù)重試寫入。
同樣我們也注意到read buffer的內(nèi)容可能會(huì)是空的系枪。這樣套接字的讀操作(一般是讀一個(gè)定長(zhǎng)的字節(jié)數(shù)組)也會(huì)阻塞雀哨,直到read buffer中有了足夠的內(nèi)容(填充滿字節(jié)數(shù)組)才會(huì)返回。有了NIO私爷,就可以有多少讀多少雾棺,無(wú)須阻塞了。讀不夠的衬浑,后續(xù)會(huì)繼續(xù)嘗試讀取捌浩。
圖片中并沒有展現(xiàn)數(shù)據(jù)的確認(rèn)過(guò)程(ack)。比如當(dāng)寫緩沖的內(nèi)容拷貝到網(wǎng)卡后工秩,是不會(huì)立即從寫緩沖中將這些拷貝的內(nèi)容移除的尸饺,而要等待對(duì)方的ack過(guò)來(lái)之后才會(huì)移除。如果網(wǎng)絡(luò)狀況不好助币,ack遲遲不過(guò)來(lái)浪听,寫緩沖很快就會(huì)滿的。
如果讀緩沖滿了的話眉菱,一般的做法就是丟棄掉緩沖區(qū)的內(nèi)容而且不給對(duì)方ack迹栓,對(duì)方如果發(fā)現(xiàn)ack遲遲沒有來(lái),就會(huì)重發(fā)消息俭缓。那緩沖為什么會(huì)滿克伊?是因?yàn)橄⒔邮辗教幚淼穆l(fā)送方生產(chǎn)的消息太快了,這時(shí)候tcp協(xié)議就會(huì)有個(gè)動(dòng)態(tài)窗口調(diào)整算法來(lái)限制發(fā)送方的發(fā)送速率华坦,使得收發(fā)效率趨于匹配答毫。