UNIX網(wǎng)絡(luò)編程5種I/O模型
-
I/O 復(fù)用模型(最大的優(yōu)勢(shì)是多路復(fù)用)
Linux提供select/poll,進(jìn)程通過將一個(gè)或多個(gè)fd傳遞給select或poll系統(tǒng)調(diào)用,阻塞在select操作上,這樣select/poll可以幫我們偵測(cè)多個(gè)fd是否處于就緒狀態(tài)。select/poll是順序掃描fd是否就緒,而且支持的fd數(shù)量有限,因此它的使用受到了一些制約。Linux還提供了一個(gè)epoll系統(tǒng)調(diào)用,epoll使用基于事件驅(qū)動(dòng)方式代替順序掃描,因此性能更高。當(dāng)有fd就緒時(shí),立即回調(diào)函數(shù)rollback
-
I/O 多路復(fù)用技術(shù)
- I/O 多路復(fù)用技術(shù)通過把多個(gè)I/O 的阻塞復(fù)用到同一個(gè)select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求
-
目前支持I/O多路復(fù)用的系統(tǒng)調(diào)用有select、pselect、poll、epoll,在 Linux網(wǎng)絡(luò)編程過程中,很長一段時(shí)間都使用select做輪詢和網(wǎng)絡(luò)事件通知,然而select的些固有缺陷導(dǎo)致了它的應(yīng)用受到了很大的限制,最終Linux不得不在新的內(nèi)核版本中尋找select的替代方案,最終選擇了epoll
-
支持一個(gè)進(jìn)程打開的socket描述符(FD ) 不受限制(僅受限于操作系統(tǒng)的最大文
件句柄數(shù))
- select最大的缺陷就是單個(gè)進(jìn)程所打開的FD是有一定限制的,它由FD_SETSIZE設(shè)
置,默認(rèn)值是1024,選擇修改這個(gè)宏需要重新編譯內(nèi)核且網(wǎng)絡(luò)效率會(huì)下降
- cat /proc/sys/fs/file- max
-
I/O 效率不會(huì)隨著FD數(shù)目的增加而線性下降
- 由于網(wǎng)絡(luò)延時(shí)或者鏈路空閑,任一時(shí)刻只有少部分的socket是 “活躍”的,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。epoll不存在這個(gè)問題,它只會(huì)對(duì)“活躍”的socket進(jìn)行操作
-
使用mmap加速內(nèi)核與用戶空間的消息傳遞
- 無論是select、poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必
要的內(nèi)存復(fù)制就顯得非常重要,epoll是通過內(nèi)核和用戶空間mmap同一塊內(nèi)存來實(shí)現(xiàn)的
- mmap-map files or devices into memory
- epoll的API更加簡(jiǎn)單
- 用來克服select/poll缺點(diǎn)的方法不只有epoll, epoll只是一種Linux的實(shí)現(xiàn)方案。在 freeBSD下有kqueue
-
從5種I/O模型來看,其實(shí)都涉及到兩個(gè)階段
- 等待數(shù)據(jù)準(zhǔn)備就緒
-
數(shù)據(jù)從內(nèi)核復(fù)制到用戶空間
- 對(duì)于阻塞io,調(diào)用recvfrom,阻塞直到第二個(gè)階段完成或者錯(cuò)誤才返回
- 對(duì)于非阻塞io,調(diào)用recvfrom,如果緩沖區(qū)沒有數(shù)據(jù)則直接返回錯(cuò)誤,一般都對(duì)非阻塞I/O 模型進(jìn)行輪詢檢査這個(gè)狀態(tài),看內(nèi)核是不是有數(shù)據(jù)到來;數(shù)據(jù)準(zhǔn)備后,第二個(gè)階段也是阻塞的
-
對(duì)于I/O復(fù)用模型,第一個(gè)階段進(jìn)程阻塞在select調(diào)用,等待1個(gè)或多個(gè)套接字(多路)變?yōu)榭勺x,而第二個(gè)階段是阻塞的
- 這里進(jìn)程是被select阻塞但不是被socket io阻塞
-
java nio實(shí)現(xiàn)
- 是否阻塞configureBlocking(boolean block)
- selector事件到來時(shí)(只是判斷是否可讀/可寫)->具體的讀寫還是由阻塞和非阻塞決定->如阻塞模式下,如果輸入流不足r字節(jié)則進(jìn)入阻塞狀態(tài),而非阻塞模式下則奉行能讀到多少就讀到多少的原則->立即返回->
- 同理寫也是一樣->selector只是通知可寫->但是能寫多少數(shù)據(jù)也是有阻塞和非阻塞決定->如阻塞模式->如果底層網(wǎng)絡(luò)的輸出緩沖區(qū)不能容納r個(gè)字節(jié)則會(huì)進(jìn)入阻塞狀態(tài)->而非阻塞模式下->奉行能輸出多少就輸出多少的原則->立即返回
- 對(duì)于accept->阻塞模式->沒有client連接時(shí),線程會(huì)一直阻塞下去->而非阻塞時(shí)->沒有客戶端連接->方法立刻返回null->
- 對(duì)于信號(hào)驅(qū)動(dòng)I/O模型,應(yīng)用進(jìn)程建立SIGIO信號(hào)處理程序后立即返回,非阻塞,數(shù)據(jù)準(zhǔn)備就緒時(shí),生成SIGIO信號(hào)并通過信號(hào)回調(diào)應(yīng)用程序通過recvfrom來讀取數(shù)據(jù),第二個(gè)階段也是阻塞的
- 而對(duì)于異步I/O模型來說,第二個(gè)階段的時(shí)候內(nèi)核已經(jīng)通知我們數(shù)據(jù)復(fù)制完成了
-
Java NIO的核心類庫多路復(fù)用器Selector就是基于epoll的多路復(fù)用技術(shù)實(shí)現(xiàn)
-
Enhancements in JDK 6 Release
-
A new java.nio.channels.SelectorProvider implementation that is based on the Linux epoll event notification facility is included. The epoll facility is available in the Linux 2.6, and newer, kernels. The new epoll-based SelectorProvider implementation is more scalable than the traditional poll-based SelectorProvider implementation when there are thousands of SelectableChannels registered with a Selector. The new SelectorProvider implementation will be used by default when the 2.6 kernel is detected. The poll-based SelectorProvider will be used when a pre-2.6 kernel is detected.
- 即JDK6版本中默認(rèn)的SelectorProvider即為epoll(Linux 2.6 kernal)
- macosx-sun.nio.ch.KQueueSelectorProvider
- solaris-sun.nio.ch.DevPollSelectorProvider
-
linux
- 2.6以上版本-sun.nio.ch.EPollSelectorProvider
- 以下版本-sun.nio.ch.PollSelectorProvider
- windows-sun.nio.ch.WindowsSelectorProvider
-
Oracle jdk會(huì)自動(dòng)選擇合適的Selector,如果想設(shè)置特定的Selector
- -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
-
Netty Native transports
- Since 4.0.16, Netty provides the native socket transport for Linux using JNI. This transport has higher performance and produces less garbage
-
Netty's epoll transport uses epoll edge-triggered while java's nio library uses level-triggered. Beside this the epoll transport expose configuration options that are not present with java's nio like TCPCORK, SOREUSEADDR and more.
- 即Netty的Linux原生傳輸層使用了epoll邊緣觸發(fā)
- 而jdk的nio類庫使用的是epoll水平觸發(fā)
-
epoll ET(Edge Triggered) vs LT(Level Triggered)
- 簡(jiǎn)單來說就是當(dāng)邊緣觸發(fā)時(shí),只有 fd 變成可讀或可寫的那一瞬間才會(huì)返回事件。當(dāng)水平觸發(fā)時(shí),只要 fd 可讀或可寫,一直都會(huì)返回事件
- 簡(jiǎn)單地說,如果你有數(shù)據(jù)過來了,不去取LT會(huì)一直騷擾你,提醒你去取,而ET就告訴你一次,愛取不取,除非有新數(shù)據(jù)到來,否則不再提醒
- Nginx大部分event采用epoll EPOLLET(邊沿觸發(fā))的方法來觸發(fā)事件,只有l(wèi)isten端口的讀事件是EPOLLLT(水平觸發(fā)).對(duì)于邊沿觸發(fā),如果出現(xiàn)了可讀事件,必須及時(shí)處理,否則可能會(huì)出現(xiàn)讀事件不再觸發(fā),連接餓死的情況
-
Java7 NIO2
-
implemented using IOCP on Windows
- WindowsAsynchronousSocketChannelImpl implements Iocp.OverlappedChannel
- 即nio2在windows的底層實(shí)現(xiàn)是iocp
-
Linux using epoll
- UnixAsynchronousSocketChannelImpl implements Port.PollableChannel
-
即nio2在linux 2.6后的底層實(shí)現(xiàn)還是epoll
- 通過epoll模擬異步
-
個(gè)人認(rèn)為也許linux內(nèi)核本身的aio實(shí)現(xiàn)方案其實(shí)并不是很完善,或多或少有這樣或者那樣的問題,即使用了aio,也沒有明顯的性能優(yōu)勢(shì)
- Not faster than NIO (epoll) on unix systems (which is true)
-
Reactor/Proactor
- 兩種IO設(shè)計(jì)模式
-
Reactor-Dispatcher/Notifier
- Don't call us, we'll call you
- Proactor-異步io
- Reactor通過某種變形,可以將其改裝為Proactor,在某些不支持異步I/O的系統(tǒng)上,也可以隱藏底層的實(shí)現(xiàn),利于編寫跨平臺(tái)代碼
-
參考
Java I/O類庫的發(fā)展和改進(jìn)
-
BIO(blocking)
-
采用BIO通信模型的服務(wù)端,通常由一個(gè)獨(dú)立的Acceptor線程負(fù)責(zé)監(jiān)聽客戶端的連接,它接收到客戶端連接請(qǐng)求之后為每個(gè)客戶端創(chuàng)建一個(gè)新的線程進(jìn)行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀
- 該模型最大的問題就是缺乏彈性伸縮能力,當(dāng)客戶端并發(fā)訪問量增加后,服務(wù)端的線程個(gè)數(shù)和客戶端并發(fā)訪問數(shù)呈1: 1 的正比關(guān)系,由于線程是Java虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹之后,系統(tǒng)的性能將急劇下降,隨著并發(fā)訪問量的繼續(xù)增大,系統(tǒng)會(huì)發(fā)生線程堆棧溢出、創(chuàng)建新線程失敗等問題,并最終導(dǎo)致進(jìn)程宕機(jī)或者值死,不能對(duì)外提供服務(wù)
- ServerSocket/Socket/輸入輸出流(阻塞)
-
偽異步I/O
- 為了改進(jìn)一線程一連接模型,后來又演進(jìn)出了一種通過線程池或者消息隊(duì)列實(shí)現(xiàn)1個(gè)或者多個(gè)線程處理N個(gè)客戶端的模型
- 采用線程池和任務(wù)隊(duì)列可以實(shí)現(xiàn)一種叫做偽異步的I/O通信框架
-
當(dāng)有新的客戶端接入時(shí),將客戶端的Socket封裝成一個(gè)Task,投遞到后端的線程池中進(jìn)行處理,JDK的線程池維護(hù)一個(gè)消息隊(duì)列和N個(gè)活躍線程,對(duì)消息隊(duì)列中的任務(wù)進(jìn)行處理
- 當(dāng)對(duì)方發(fā)送請(qǐng)求或者應(yīng)答消息比較緩慢,或者網(wǎng)絡(luò)傳輸較慢時(shí),讀取輸入流一方的通信線程將被長時(shí)間阻塞,如果對(duì)方要60s 才能夠?qū)?shù)據(jù)發(fā)送完成,讀取一方的I/O線程也將會(huì)被同步阻塞60s, 在此期間,其他接入消息只能在消息隊(duì)列中排隊(duì)
- 當(dāng)消息的接收方處理緩慢的時(shí)候,將不能及時(shí)地從TCP緩沖區(qū)讀取數(shù)據(jù),這將會(huì)導(dǎo)致發(fā)送方的TCP window size( 滑動(dòng)窗口)不斷減小,直到為0,雙方處于Keep-Alive狀態(tài),消息發(fā)送方將不能再向TCP緩沖區(qū)寫入消息
-
NIO
- SocketChannel和ServerSocketChannel,支持阻塞和非阻塞兩種模式
-
Buffer/Channel/Selector(多路復(fù)用器,可同時(shí)輪詢多個(gè)Channel)
-
java.nio.ByteBuffer的幾個(gè)常用方法
- flip、clear、compact、mark、rewind、hasRemaining、isDirect等
- 客戶端發(fā)起的連接操作是異步的
- SocketChannel的讀寫操作都是異步的,如果沒有可讀寫的數(shù)據(jù)它不會(huì)同步等待,直接返回,這樣I/O 通信線程就可以處理其他的鏈路,不需要同步等待這個(gè)鏈路可用
- JDK的 Selector在 Linux等主流操作系統(tǒng)上通過epoll實(shí)現(xiàn),它沒有連接句柄數(shù)的限制
-
AIO
- AsynchronousServerSocketChannel、AsynchronousSocketChannel
-
CompletionHandler<V,A>
- V The result type of the I/O operation
- A The type of the object attached to the I/O operation
- 既然已經(jīng)接收客戶端成功了,為什么還要再次調(diào)用accept方法呢?原因是這樣的:調(diào)用AsynchronousServerSocketChannel的accept方法后,如果有新的客戶端連接接入,系統(tǒng)將回調(diào)我們傳入的CompletionHandler實(shí)例的completed方法,表示新的客戶端已經(jīng)接入成功。因?yàn)橐粋€(gè)AsynchronousServerSocketChannel可以接收成千上萬個(gè)客戶端,所以需要繼續(xù)調(diào)用它的accept方法,接收其他的客戶端連接,最終形成一個(gè)循環(huán)。每當(dāng)接收一個(gè)客戶讀連接成功之后,再異步接收新的客戶端連接
-
不選擇Java原生NIO編程的原因
- N10的類庫和API繁雜,使用麻煩
- 需要具備其他的額外技能做鋪墊,例如熟悉Java多線程
- 可靠性能力補(bǔ)齊,工作景和難度都非常大。例如客戶端面臨斷連重連、網(wǎng)絡(luò)閃斷、半包讀寫、失敗緩存、網(wǎng)絡(luò)擁塞和異常碼流的處理等問題
- JDK NIO 的 BUG, 例如臭名昭著的epollbug, 它會(huì)導(dǎo)致Selector空輪詢,最終導(dǎo)致CPU100%
-
為什么選擇Netty
- 健壯性、功能、性能、可定制性和可擴(kuò)展性在同類框架中都是首屈一指的,它已經(jīng)得到成百上千的商用項(xiàng)目驗(yàn)證
- API使用簡(jiǎn)單
- 預(yù)置了多種編解碼功能,支持多種主流協(xié)議
- 可以通過ChannelHand丨er對(duì)通信框架進(jìn)行靈活地?cái)U(kuò)展
- 性能高
- Netty修復(fù)了己經(jīng)發(fā)現(xiàn)的所有JDKNIO BUG
- 社區(qū)活躍,版本迭代周期短
- 經(jīng)歷了大規(guī)模的商業(yè)應(yīng)用考驗(yàn),質(zhì)量得到驗(yàn)證
Netty 入門
- ServerBootstrap、EventLoopGroup(boss)、EventLoopGroup(worker)、NioServerSocketChannel、ChannelOption、ChannelInitializer、ChannelPipeline、ChannelFuture、ChannelHandlerAdapter、ChannelHandlerContext
- Bootstrap、NioSocketChannel
- try/finally、shutdownGracefully(boss、worker)
- ChannelHandlerContext的 flush方法,它的作用是將消息發(fā)送隊(duì)列中的消息寫入SocketChannel中發(fā)送給對(duì)方.從性能角度考慮,為了防止頻繁地喚醒Selector進(jìn)行消息發(fā)送,Netty的 write方法并不直接將消息寫入SocketChannel中,調(diào)用write方法只是把待發(fā)送的消息放到發(fā)送緩沖數(shù)組中,再通過調(diào)用flush方法,將發(fā)送緩沖區(qū)中的消息全部寫到SocketChannel中
- 基于Netty開發(fā)的都是非Web的Java應(yīng)用,它的打包形態(tài)非常簡(jiǎn)單,就是一個(gè)普通的.jar 包,通常可以使用Eclipse、Ant、Ivy、Gradle等進(jìn)行構(gòu)建
TCP 粘包/拆包問題的解決之道
- TCP是個(gè)“流”協(xié)議,所謂流,就是沒有界限的一串?dāng)?shù)據(jù)。大家可以想想河里的流水,它們是連成一片的,其間并沒有分界線。TCP底層并不了解上層業(yè)務(wù)數(shù)據(jù)的具體含義,它會(huì)根據(jù)TCP緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分,所以在業(yè)務(wù)上認(rèn)為 , 一個(gè)完 整的包可能會(huì)被 TCP拆分成多個(gè)包進(jìn)行發(fā)送,也有可能把多個(gè)小的包封裝成個(gè)大的數(shù)據(jù)包發(fā)送,這就是所謂的TCP粘包和拆包問題。
-
由于底層的TCP無法理解上層的業(yè)務(wù)數(shù)據(jù),所以在底層是無法保證數(shù)據(jù)包不被拆分和重組的,這個(gè)問題只能通過上層的應(yīng)用協(xié)議棧設(shè)計(jì)來解決
- 消息定長,例如每個(gè)報(bào)文的大小為固定長度200字節(jié),如果不夠,空位補(bǔ)空格
- 在包尾增加回車換行符進(jìn)行分割,例如FTP協(xié)議
- 將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段,通常設(shè)計(jì)思路為消息頭的第一個(gè)字段使用int32來表示消息的總長度
- 沒有考慮讀半包問題,這在功能測(cè)試時(shí)往往沒有問題,但是一旦壓力上來,或者發(fā)送大報(bào)文之后,就會(huì)存在粘包/拆包問題,如循環(huán)發(fā)送100條消息,則可能會(huì)出現(xiàn)TCP粘包
-
為了解決TCP粘包/拆包導(dǎo)致的半包讀寫問題,Netty默認(rèn)提供了多種編解碼器用于處理半包
-
LineBasedFrameDecoder
- A decoder that splits the received {@link ByteBuf}s on line endings
-
StringDecoder
- A decoder that splits the received {@link ByteBuf}s on line endings
分隔符和定長解碼器的應(yīng)用
-
DelimiterBasedFrameDecoder
- A decoder that splits the received {@link ByteBuf}s by one or more delimiters
-
FixedLengthFrameDecoder
- A decoder that splits the received {@link ByteBuf}s by the fixed number of bytes
編解碼技術(shù)
-
基于Java提供的對(duì)象輸入/輸出流ObjectlnputStream和 ObjectOutputStream,可以直接把Java對(duì)象作為可存儲(chǔ)的字節(jié)數(shù)組寫入文件 ,也可以傳輸?shù)骄W(wǎng)絡(luò)上,Java序列化的目的:
- 網(wǎng)絡(luò)傳輸
- 對(duì)象持久化
-
Java序列化的缺點(diǎn)
- 無法跨語言
-
序列化后的碼流太大
-
對(duì)于字符串
- byte[] value = this.userName.getBytes();
- buffer.putInt(value.length);
- buffer.put(value);
- 序列化性能太低
-
業(yè)界主流的編解碼框架
- Google的Protobuf
- Facebook的Thrift
-
JBoss Marshalling
- JBoss Marshalling是一個(gè)Java對(duì)象的序列化API包,修正了 JDK自帶的序列化包的很多問題,但又保持跟java.io.Serializable接口的兼容
MessagePack編解碼
-
MessagePack介紹
- It's like JSON. but fast and small
- MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller
- http://msgpack.org/
- 提供了對(duì)多語言的支持
-
API介紹
// Create serialize objects.
List<String> src = new ArrayList<String>();
src. add (,,msgpackw);
src.add("kumofs");
src.add("viver">;
MessagePack msgpack = new MessagePack();
// Serialize
byte[] raw = msgpack.write(src);
// Deserialize directly using a template
L±8t<String> dstl = msgpack. read (raw, Ten 5 >lates . tList (Ten^>lates. TString));
-
MessagePack編碼器和解碼器開發(fā)
- MessageToByteEncoder<I
- ByteToMessageDecoder、MessageToMessageDecoder<I
-
LengthFieldBasedFrameDecoder extends ByteToMessageDecoder
- A decoder that splits the received {@link ByteBuf}s dynamically by the value of the length field in the message
- public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip, boolean failFast)
- 功能很強(qiáng)大,可指定消息長度字段的偏移等,而不僅僅是消息頭的第一個(gè)字段就是長度
-
LengthFieldPrepender extends MessageToMessageEncoder
- An encoder that prepends the length of the message.
- 可自動(dòng)前面加上消息長度字段
Google Protobuf 編解碼
-
主要使用了netty默認(rèn)提供的關(guān)于protobuf的編解碼器
-
ProtobufVarint32FrameDecoder extends ByteToMessageDecoder
- A decoder that splits the received {@link ByteBuf}s dynamically by the value of the Google Protocol Buffers
-
ProtobufVarint32LengthFieldPrepender extends MessageToByteEncoder
- An encoder that prepends the the Google Protocol Buffers
-
ProtobufEncoder extends MessageToMessageEncoder
- Encodes the requested Google Protocol Buffers Message And MessageLite into a {@link ByteBuf}
-
ProtobufDecoder extends MessageToMessageDecoder
- Decodes a received {@link ByteBuf} into a Google Protocol Buffers Message And MessageLite
- 注意其構(gòu)造函數(shù)要傳一個(gè)MessageLite對(duì)象,即協(xié)議類型,用來反序列化
BEFORE DECODE (302 bytes) AFTER DECODE (300 bytes)
+--------+---------------+ +---------------+
| Length | Protobuf Data |----->| Protobuf Data |
| 0xAC02 | (300 bytes) | | (300 bytes) |
+--------+---------------+ +---------------+
BEFORE ENCODE (300 bytes) AFTER ENCODE (302 bytes)
+---------------+ +--------+---------------+
| Protobuf Data |-------------->| Length | Protobuf Data |
| (300 bytes) | | 0xAC02 | (300 bytes) |
+---------------+ +--------+---------------+
-
Protobuf的使用注意事項(xiàng)
-
ProtobufDecoder僅僅負(fù)責(zé)解碼,它不支持讀半包。因此,在 ProtobufDecoder前面,
一定要有能夠處理讀半包的解碼器
- 使用Netty提供的ProtobufVarint32FrameDecoder,它可以處理半包消息
- 繼承Netty提供的通用半包解碼器LengthFieldBasedFrameDecoder
- 繼承ByteToMessageDecoder類,自己處理半包消息
JBoss Marshalling 編解碼
- JBoss的Marshalling完全兼容JDK序列化
-
MarshallingDecoder extends LengthFieldBasedFrameDecoder
- Decoder which MUST be used with {@link MarshallingEncoder}
- 需要傳入U(xiǎn)nmarshallerProvider和maxObjectSize
-
MarshallingEncoder extends MessageToByteEncoder
posted on 2017-01-19 22:00
landon 閱讀(3365)
評(píng)論(0) 編輯 收藏 所屬分類:
Book