本文同時(shí)發(fā)布至我的個(gè)人博客,點(diǎn)擊進(jìn)入我的個(gè)人博客閱讀。本博客供技術(shù)交流與經(jīng)驗(yàn)分享,可自由轉(zhuǎn)載。轉(zhuǎn)載請(qǐng)?jiān)谠u(píng)論區(qū)或私信簡單通知,感謝!
近期為了應(yīng)對(duì)校招面試,比較有針對(duì)性地做了兩個(gè)技術(shù)項(xiàng)目:一個(gè)是基于 Java NIO 實(shí)現(xiàn)的簡易版非阻塞 Http 服務(wù)器,另一個(gè)是基于 Spring Boot + Websocket 實(shí)現(xiàn)的網(wǎng)絡(luò)聊天室。與以往的項(xiàng)目總結(jié)不同,這次我會(huì)忽略一些代碼實(shí)現(xiàn)細(xì)節(jié),把更多的篇幅用來總結(jié)開發(fā)過程中我遇到的難題以及在面試中面試官提出的相關(guān)問題。
基于 Java NIO 實(shí)現(xiàn)的簡易版非阻塞 Http 服務(wù)器
首先需要強(qiáng)調(diào)的是,這種 Http 服務(wù)器的實(shí)現(xiàn)是非常樸素直接的,沒有考慮很多具體實(shí)踐中的問題。最近在看《Netty In Action》才越來越感覺之前做項(xiàng)目時(shí)想法上的過于單純。但項(xiàng)目本身其實(shí)十分適合初學(xué)者進(jìn)階的,因?yàn)槠渲猩婕暗降闹R(shí):Java NIO、網(wǎng)絡(luò)多路復(fù)用、Http 報(bào)文解析、緩沖區(qū)設(shè)計(jì),都是十分重要的基礎(chǔ)問題,也是在校招技術(shù)面試中面試官喜歡深入考察的問題。在開發(fā)過程中,我借鑒了 Java NIO: Non-blocking Server非阻塞服務(wù)器這篇文章的思路,上面提及的幾個(gè)關(guān)鍵問題文章中也有較為詳細(xì)的解析。
項(xiàng)目基本架構(gòu)

Java BIO/NIO/AIO
關(guān)于 Java BIO/NIO/AIO 這三者的區(qū)別,個(gè)人覺得 Java BIO, NIO, AIO understanding 這篇文章總結(jié)得十分簡潔到位,作者分別從編程、原理、底層三方面進(jìn)行總結(jié)。編程方面作者講解的十分到位,而要理解原理部分可能需要一些 Unix 網(wǎng)絡(luò)編程的相關(guān)知識(shí),以下就做一些網(wǎng)絡(luò)編程相關(guān)的補(bǔ)充:
Unix 網(wǎng)絡(luò)編程中將 IO 模型分為五類:阻塞 IO、非阻塞 IO(輪詢)、IO 復(fù)用、信號(hào)驅(qū)動(dòng)式 IO、異步 IO。其中非阻塞 IO(輪詢)、IO 復(fù)用、信號(hào)驅(qū)動(dòng)式 IO 都可以理解為非阻塞 IO 的不同形式實(shí)現(xiàn)。(圖解UNIX的I/O模型一文可以讓你快速理解 Unix 中的 IO 模型)
無論是屬于哪種 IO 模型,其 IO 過程都可以分為兩個(gè)階段: IO request(數(shù)據(jù)請(qǐng)求)和 IO operation(數(shù)據(jù)復(fù)制)兩個(gè)階段。
阻塞 IO 與非阻塞 IO 的區(qū)別在于:使用阻塞 IO 時(shí),線程在 IO request 和 IO operation 階段都會(huì)進(jìn)入阻塞;而使用非阻塞 IO 時(shí),線程只在 IO operation 階段進(jìn)入阻塞,IO request 無需阻塞。
同步 IO 與異步 IO 的區(qū)別在于:同步 IO 在 IO operation 階段會(huì)進(jìn)入阻塞,而異步 IO 在 IO request 和 IO operation 階段都不會(huì)進(jìn)入阻塞。
按照 Unix 網(wǎng)絡(luò)編程中對(duì) IO 模型的分類,Java BIO/NIO/AIO 的實(shí)現(xiàn)分別對(duì)應(yīng)了阻塞 IO/IO 多路復(fù)用/異步 IO 這三種模型。
BIO vs NIO
Java AIO(在 JDK 1.7 中也稱為 NIO.2),在性能上相較于 NIO 并沒有帶來提升,所以在 Netty 4.0.0 中也移除了對(duì) AIO 的支持。無論是在實(shí)際應(yīng)用中還是在技術(shù)面試中都較少提及 AIO,這里我們重點(diǎn)比較一下 BIO 與 NIO 的區(qū)別:
處理的對(duì)象:BIO 直接面向 Socket 的字節(jié)流,每次從流中讀一個(gè)或多個(gè)字節(jié),直到讀取完所有字節(jié);NIO 面向緩沖塊(Block),需要時(shí)可以在緩沖區(qū)中前后移動(dòng)處理,這增加了處理過程的靈活性。
阻塞:BIO 必須要對(duì)線程進(jìn)行阻塞,NIO 無需阻塞,一個(gè)單獨(dú)的線程可以管理多個(gè)輸入和輸出通道。
選擇器:Java NIO的選擇器允許一個(gè)單獨(dú)的線程同時(shí)監(jiān)視多個(gè)通道,可以注冊(cè)多個(gè)通道到同一個(gè)選擇器上,然后使用一個(gè)單獨(dú)的線程來“選擇”已經(jīng)就緒的通道。
零拷貝:Java NIO中提供的
FileChannel擁有transferTo和transferFrom兩個(gè)方法,可直接把FileChannel中的數(shù)據(jù)拷貝到另外一個(gè) Channel,或者直接把另外一個(gè) Channel 中的數(shù)據(jù)拷貝到FileChannel。通過該方法傳輸數(shù)據(jù)并不需要將源數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài),再從用戶態(tài)拷貝到目標(biāo)通道的內(nèi)核態(tài),同時(shí)也避免了兩次用戶態(tài)和內(nèi)核態(tài)間的上下文切換,也即使用了“零拷貝”。
總結(jié)自:Java進(jìn)階(五)Java I/O模型從 BIO到 NIO和 Reactor模式
HTTP 報(bào)文及解析
項(xiàng)目中對(duì) HTTP 協(xié)議的支持也做了簡化處理:只判別了 GET、POST、HEAD、PUT、DELETE 五種方法,主要是基于 HTTP 的 Content-Length 字段來實(shí)現(xiàn) HTTP 報(bào)文切割(處理 TCP 粘包問題)。但是在面試中,HTTP 協(xié)議幾乎是所有面試官會(huì)深究的一部分內(nèi)容。大概涉及的問題如下:
-
HTTP 報(bào)文格式:
image HTTP Method 有哪些?
HTTP 常見狀態(tài)碼有哪些?
HTTP 建立 TCP 長連接?(Connection:Keep-Alive)
Message 緩沖區(qū)設(shè)計(jì)
讀取不完整的 message:每次 Reader 從 Channel 讀取一個(gè)數(shù)據(jù)塊后,先通過一個(gè) Http 報(bào)文 parser 來確定是否有一個(gè)完整的 message。若有,則讀取一個(gè)完整的 message 后將剩余部分緩存起來;若無,則直接將整個(gè)數(shù)據(jù)塊緩存起來。緩存的數(shù)據(jù)塊將在下次讀取時(shí)與下次讀取的數(shù)據(jù)塊合并,再進(jìn)行重新的 parsing。
message buffer 的大?。篗essageBuffer 實(shí)現(xiàn)了一個(gè)容量可伸縮的 message buffer。它提供三種大小的 buffer,4KB/128KB/1MB。初始時(shí) buffer 默認(rèn)為 4KB,若空間不足時(shí),MessageBuffer 內(nèi)部的方法會(huì)自動(dòng)將 buffer 擴(kuò)容。
message buffer 的讀寫:仿寫了
nio.Buffer中的flip()函數(shù)(readPos和writePos),在 message buffer 的讀寫操作中調(diào)用flip()來避免錯(cuò)讀、漏讀。
生產(chǎn)者-消費(fèi)者隊(duì)列
用一個(gè) ArrayBlockingQueue 來存放 socket,創(chuàng)建兩個(gè)線程,acceptor 和 processor 分別使用其 put / take 操作來進(jìn)行生產(chǎn)和消費(fèi)。
關(guān)于 ArrayBlockingQueue:1. 基于數(shù)組,直接將對(duì)象放入和取出隊(duì)列。(LinkedBlockingQueue 基于單向鏈表,放入與取出時(shí)操作 Node);2. 基于一個(gè) ReentrantLock 和兩個(gè) Condition(notEmpty 和 notFull) 實(shí)現(xiàn)。(LinkedBlockingQueue 基于兩個(gè) ReentrantLock :putLock 和 takeLock 和兩個(gè) Condition:notEmpty 和 notFull 實(shí)現(xiàn))
基于 Spring Boot + Websocket 實(shí)現(xiàn)的網(wǎng)絡(luò)聊天室
項(xiàng)目實(shí)現(xiàn)了一個(gè)簡單的網(wǎng)絡(luò)聊天室,基于 Spring Boot 搭建后臺(tái),基于 Websocket 與 STOMP 協(xié)議實(shí)現(xiàn)即時(shí)通訊,使用 Spring Security 與 Spring Oauth 實(shí)現(xiàn)用戶登錄,基于 JWT 實(shí)現(xiàn)對(duì)單點(diǎn)登錄的支持。
Oauth 驗(yàn)證流程

Websocket 協(xié)議
JWT
JWT(Json Web Token) 一個(gè)非常輕巧的規(guī)范。這個(gè)規(guī)范允許我們使用JWT在用戶和服務(wù)器之間傳遞安全可靠的信息。
字符串形式,三部分組成:頭部(Header)、載荷(Payload)、簽名(Signature)
-
頭部:用于描述 JWT
{ "typ": "JWT", "alg": "HS256" // 指定簽名的加密算法 } -
載荷:真正需要傳遞的內(nèi)容 + 部分其他信息
{ "iss": "User JWT", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "sub@example.com", "from_user": "B", "target_user": "A" } 簽名:用于在服務(wù)端判斷 JWT 的內(nèi)容是否經(jīng)過篡改,使用 JWT 頭部指定的加密算法。
格式:header.payload.signature(先通過 base64 將 JSON 轉(zhuǎn)化為字符串)
載荷內(nèi)容可以通過 base64 反編碼獲得,所以不應(yīng)用 JWT 傳輸敏感數(shù)據(jù)。
使用場(chǎng)景:添加好友、創(chuàng)建訂單、實(shí)現(xiàn)單點(diǎn)登錄
