UNIX網絡編程5種I/O模型
-
I/O 復用模型(最大的優勢是多路復用)
Linux提供select/poll,進程通過將一個或多個fd傳遞給select或poll系統調用,阻塞在select操作上,這樣select/poll可以幫我們偵測多個fd是否處于就緒狀態。select/poll是順序掃描fd是否就緒,而且支持的fd數量有限,因此它的使用受到了一些制約。Linux還提供了一個epoll系統調用,epoll使用基于事件驅動方式代替順序掃描,因此性能更高。當有fd就緒時,立即回調函數rollback
-
I/O 多路復用技術
- I/O 多路復用技術通過把多個I/O 的阻塞復用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求
-
目前支持I/O多路復用的系統調用有select、pselect、poll、epoll,在 Linux網絡編程過程中,很長一段時間都使用select做輪詢和網絡事件通知,然而select的些固有缺陷導致了它的應用受到了很大的限制,最終Linux不得不在新的內核版本中尋找select的替代方案,最終選擇了epoll
-
支持一個進程打開的socket描述符(FD ) 不受限制(僅受限于操作系統的最大文
件句柄數)
- select最大的缺陷就是單個進程所打開的FD是有一定限制的,它由FD_SETSIZE設
置,默認值是1024,選擇修改這個宏需要重新編譯內核且網絡效率會下降
- cat /proc/sys/fs/file- max
-
I/O 效率不會隨著FD數目的增加而線性下降
- 由于網絡延時或者鏈路空閑,任一時刻只有少部分的socket是 “活躍”的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。epoll不存在這個問題,它只會對“活躍”的socket進行操作
-
使用mmap加速內核與用戶空間的消息傳遞
- 無論是select、poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必
要的內存復制就顯得非常重要,epoll是通過內核和用戶空間mmap同一塊內存來實現的
- mmap-map files or devices into memory
- epoll的API更加簡單
- 用來克服select/poll缺點的方法不只有epoll, epoll只是一種Linux的實現方案。在 freeBSD下有kqueue
-
從5種I/O模型來看,其實都涉及到兩個階段
- 等待數據準備就緒
-
數據從內核復制到用戶空間
- 對于阻塞io,調用recvfrom,阻塞直到第二個階段完成或者錯誤才返回
- 對于非阻塞io,調用recvfrom,如果緩沖區沒有數據則直接返回錯誤,一般都對非阻塞I/O 模型進行輪詢檢査這個狀態,看內核是不是有數據到來;數據準備后,第二個階段也是阻塞的
-
對于I/O復用模型,第一個階段進程阻塞在select調用,等待1個或多個套接字(多路)變為可讀,而第二個階段是阻塞的
- 這里進程是被select阻塞但不是被socket io阻塞
-
java nio實現
- 是否阻塞configureBlocking(boolean block)
- selector事件到來時(只是判斷是否可讀/可寫)->具體的讀寫還是由阻塞和非阻塞決定->如阻塞模式下,如果輸入流不足r字節則進入阻塞狀態,而非阻塞模式下則奉行能讀到多少就讀到多少的原則->立即返回->
- 同理寫也是一樣->selector只是通知可寫->但是能寫多少數據也是有阻塞和非阻塞決定->如阻塞模式->如果底層網絡的輸出緩沖區不能容納r個字節則會進入阻塞狀態->而非阻塞模式下->奉行能輸出多少就輸出多少的原則->立即返回
- 對于accept->阻塞模式->沒有client連接時,線程會一直阻塞下去->而非阻塞時->沒有客戶端連接->方法立刻返回null->
- 對于信號驅動I/O模型,應用進程建立SIGIO信號處理程序后立即返回,非阻塞,數據準備就緒時,生成SIGIO信號并通過信號回調應用程序通過recvfrom來讀取數據,第二個階段也是阻塞的
- 而對于異步I/O模型來說,第二個階段的時候內核已經通知我們數據復制完成了
-
Java NIO的核心類庫多路復用器Selector就是基于epoll的多路復用技術實現
-
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版本中默認的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會自動選擇合適的Selector,如果想設置特定的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邊緣觸發
- 而jdk的nio類庫使用的是epoll水平觸發
-
epoll ET(Edge Triggered) vs LT(Level Triggered)
- 簡單來說就是當邊緣觸發時,只有 fd 變成可讀或可寫的那一瞬間才會返回事件。當水平觸發時,只要 fd 可讀或可寫,一直都會返回事件
- 簡單地說,如果你有數據過來了,不去取LT會一直騷擾你,提醒你去取,而ET就告訴你一次,愛取不取,除非有新數據到來,否則不再提醒
- Nginx大部分event采用epoll EPOLLET(邊沿觸發)的方法來觸發事件,只有listen端口的讀事件是EPOLLLT(水平觸發).對于邊沿觸發,如果出現了可讀事件,必須及時處理,否則可能會出現讀事件不再觸發,連接餓死的情況
-
Java7 NIO2
-
implemented using IOCP on Windows
- WindowsAsynchronousSocketChannelImpl implements Iocp.OverlappedChannel
- 即nio2在windows的底層實現是iocp
-
Linux using epoll
- UnixAsynchronousSocketChannelImpl implements Port.PollableChannel
-
即nio2在linux 2.6后的底層實現還是epoll
- 通過epoll模擬異步
-
個人認為也許linux內核本身的aio實現方案其實并不是很完善,或多或少有這樣或者那樣的問題,即使用了aio,也沒有明顯的性能優勢
- Not faster than NIO (epoll) on unix systems (which is true)
-
Reactor/Proactor
- 兩種IO設計模式
-
Reactor-Dispatcher/Notifier
- Don't call us, we'll call you
- Proactor-異步io
- Reactor通過某種變形,可以將其改裝為Proactor,在某些不支持異步I/O的系統上,也可以隱藏底層的實現,利于編寫跨平臺代碼
-
參考
Java I/O類庫的發展和改進
-
BIO(blocking)
-
采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀
- 該模型最大的問題就是缺乏彈性伸縮能力,當客戶端并發訪問量增加后,服務端的線程個數和客戶端并發訪問數呈1: 1 的正比關系,由于線程是Java虛擬機非常寶貴的系統資源,當線程數膨脹之后,系統的性能將急劇下降,隨著并發訪問量的繼續增大,系統會發生線程堆棧溢出、創建新線程失敗等問題,并最終導致進程宕機或者值死,不能對外提供服務
- ServerSocket/Socket/輸入輸出流(阻塞)
-
偽異步I/O
- 為了改進一線程一連接模型,后來又演進出了一種通過線程池或者消息隊列實現1個或者多個線程處理N個客戶端的模型
- 采用線程池和任務隊列可以實現一種叫做偽異步的I/O通信框架
-
當有新的客戶端接入時,將客戶端的Socket封裝成一個Task,投遞到后端的線程池中進行處理,JDK的線程池維護一個消息隊列和N個活躍線程,對消息隊列中的任務進行處理
- 當對方發送請求或者應答消息比較緩慢,或者網絡傳輸較慢時,讀取輸入流一方的通信線程將被長時間阻塞,如果對方要60s 才能夠將數據發送完成,讀取一方的I/O線程也將會被同步阻塞60s, 在此期間,其他接入消息只能在消息隊列中排隊
- 當消息的接收方處理緩慢的時候,將不能及時地從TCP緩沖區讀取數據,這將會導致發送方的TCP window size( 滑動窗口)不斷減小,直到為0,雙方處于Keep-Alive狀態,消息發送方將不能再向TCP緩沖區寫入消息
-
NIO
- SocketChannel和ServerSocketChannel,支持阻塞和非阻塞兩種模式
-
Buffer/Channel/Selector(多路復用器,可同時輪詢多個Channel)
-
java.nio.ByteBuffer的幾個常用方法
- flip、clear、compact、mark、rewind、hasRemaining、isDirect等
- 客戶端發起的連接操作是異步的
- SocketChannel的讀寫操作都是異步的,如果沒有可讀寫的數據它不會同步等待,直接返回,這樣I/O 通信線程就可以處理其他的鏈路,不需要同步等待這個鏈路可用
- JDK的 Selector在 Linux等主流操作系統上通過epoll實現,它沒有連接句柄數的限制
-
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
- 既然已經接收客戶端成功了,為什么還要再次調用accept方法呢?原因是這樣的:調用AsynchronousServerSocketChannel的accept方法后,如果有新的客戶端連接接入,系統將回調我們傳入的CompletionHandler實例的completed方法,表示新的客戶端已經接入成功。因為一個AsynchronousServerSocketChannel可以接收成千上萬個客戶端,所以需要繼續調用它的accept方法,接收其他的客戶端連接,最終形成一個循環。每當接收一個客戶讀連接成功之后,再異步接收新的客戶端連接
-
不選擇Java原生NIO編程的原因
- N10的類庫和API繁雜,使用麻煩
- 需要具備其他的額外技能做鋪墊,例如熟悉Java多線程
- 可靠性能力補齊,工作景和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等問題
- JDK NIO 的 BUG, 例如臭名昭著的epollbug, 它會導致Selector空輪詢,最終導致CPU100%
-
為什么選擇Netty
- 健壯性、功能、性能、可定制性和可擴展性在同類框架中都是首屈一指的,它已經得到成百上千的商用項目驗證
- API使用簡單
- 預置了多種編解碼功能,支持多種主流協議
- 可以通過ChannelHand丨er對通信框架進行靈活地擴展
- 性能高
- Netty修復了己經發現的所有JDKNIO BUG
- 社區活躍,版本迭代周期短
- 經歷了大規模的商業應用考驗,質量得到驗證
Netty 入門
- ServerBootstrap、EventLoopGroup(boss)、EventLoopGroup(worker)、NioServerSocketChannel、ChannelOption、ChannelInitializer、ChannelPipeline、ChannelFuture、ChannelHandlerAdapter、ChannelHandlerContext
- Bootstrap、NioSocketChannel
- try/finally、shutdownGracefully(boss、worker)
- ChannelHandlerContext的 flush方法,它的作用是將消息發送隊列中的消息寫入SocketChannel中發送給對方.從性能角度考慮,為了防止頻繁地喚醒Selector進行消息發送,Netty的 write方法并不直接將消息寫入SocketChannel中,調用write方法只是把待發送的消息放到發送緩沖數組中,再通過調用flush方法,將發送緩沖區中的消息全部寫到SocketChannel中
- 基于Netty開發的都是非Web的Java應用,它的打包形態非常簡單,就是一個普通的.jar 包,通常可以使用Eclipse、Ant、Ivy、Gradle等進行構建
TCP 粘包/拆包問題的解決之道
- TCP是個“流”協議,所謂流,就是沒有界限的一串數據。大家可以想想河里的流水,它們是連成一片的,其間并沒有分界線。TCP底層并不了解上層業務數據的具體含義,它會根據TCP緩沖區的實際情況進行包的劃分,所以在業務上認為 , 一個完 整的包可能會被 TCP拆分成多個包進行發送,也有可能把多個小的包封裝成個大的數據包發送,這就是所謂的TCP粘包和拆包問題。
-
由于底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決
- 消息定長,例如每個報文的大小為固定長度200字節,如果不夠,空位補空格
- 在包尾增加回車換行符進行分割,例如FTP協議
- 將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段,通常設計思路為消息頭的第一個字段使用int32來表示消息的總長度
- 沒有考慮讀半包問題,這在功能測試時往往沒有問題,但是一旦壓力上來,或者發送大報文之后,就會存在粘包/拆包問題,如循環發送100條消息,則可能會出現TCP粘包
-
為了解決TCP粘包/拆包導致的半包讀寫問題,Netty默認提供了多種編解碼器用于處理半包
-
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
分隔符和定長解碼器的應用
-
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
編解碼技術
-
基于Java提供的對象輸入/輸出流ObjectlnputStream和 ObjectOutputStream,可以直接把Java對象作為可存儲的字節數組寫入文件 ,也可以傳輸到網絡上,Java序列化的目的:
-
Java序列化的缺點
- 無法跨語言
-
序列化后的碼流太大
-
對于字符串
- byte[] value = this.userName.getBytes();
- buffer.putInt(value.length);
- buffer.put(value);
- 序列化性能太低
-
業界主流的編解碼框架
- Google的Protobuf
- Facebook的Thrift
-
JBoss Marshalling
- JBoss Marshalling是一個Java對象的序列化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/
- 提供了對多語言的支持
-
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編碼器和解碼器開發
- 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)
- 功能很強大,可指定消息長度字段的偏移等,而不僅僅是消息頭的第一個字段就是長度
-
LengthFieldPrepender extends MessageToMessageEncoder
- An encoder that prepends the length of the message.
- 可自動前面加上消息長度字段
Google Protobuf 編解碼
-
主要使用了netty默認提供的關于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
- 注意其構造函數要傳一個MessageLite對象,即協議類型,用來反序列化
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的使用注意事項
-
ProtobufDecoder僅僅負責解碼,它不支持讀半包。因此,在 ProtobufDecoder前面,
一定要有能夠處理讀半包的解碼器
- 使用Netty提供的ProtobufVarint32FrameDecoder,它可以處理半包消息
- 繼承Netty提供的通用半包解碼器LengthFieldBasedFrameDecoder
- 繼承ByteToMessageDecoder類,自己處理半包消息
JBoss Marshalling 編解碼
- JBoss的Marshalling完全兼容JDK序列化
-
MarshallingDecoder extends LengthFieldBasedFrameDecoder
- Decoder which MUST be used with {@link MarshallingEncoder}
- 需要傳入UnmarshallerProvider和maxObjectSize
-
MarshallingEncoder extends MessageToByteEncoder
posted on 2017-01-19 22:00
landon 閱讀(3365)
評論(0) 編輯 收藏 所屬分類:
Book