上一篇 介紹了事件監聽、責任鏈模型、socket接口和IO模型、線程模型等基本概念,以及Netty的整體結構,這篇就來說下Netty三大核心模塊之一:事件監聽和處理。
前面提到,Netty是一個NIO框架,它將IO通道的建立、可讀、可寫等狀態變化,抽象成事件,以責任鏈的方式進行傳遞,可以在處理鏈上插入自定義的Handler,對感興趣的事件進行監聽和處理。
通過介紹,你會瞭解到:
- 事件監聽和處理模型
- 事件監聽:EventLoop
- 事件處理:ChannelPipeline和ChannelHandler
- 使用Netty實現Websocket協議
事件監聽和處理模型
進行網絡編程時,一般的編寫過程是這樣的:
- 創建服務端Socket,監聽某個端口;
- 當有客戶端連接時,會創建一個新的客戶端Socket,監聽數據的可讀、可寫狀態,每一個連接請求都會創建一個客戶端Socket;
- 讀取和寫入數據都會調用Socket提供的接口,接口列表在上一篇提到過;
傳統的模型,每個客戶端Socket會創建一個單獨的線程監聽socket事件,一方面系統可創建的線程數有限,限制了併發數,一方面線程過多,線程切換頻繁,導致性能嚴重下降。
隨着操作系統IO模型的發展,可以採用多路複用IO,一個線程監聽多個Socket,另外,服務端處理客戶端連接,與客戶端Socket的監聽,可以在不同的線程進行處理。
Netty就是採用多路複用IO進行事件監聽,另外,使用不同的線程分別處理客戶端的連接、數據讀寫。
整個處理結構如下圖,簡單說明下:
- Boss EventLoopGroup主要處理客戶端的connect事件,包含多個EventLoop,每個EventLoop一個線程;
- Worker EventLoopGroup主要處理客戶端Socket的數據read、write事件,包含多個EventLoop,每個EventLoop一個線程;
- 無論是Boos還是Worker,事件的處理都是通過Channel Pipleline組織的,它是責任鏈模式的實現,包含一個或多個Handler;
- 偵聽一個端口,只會綁定到Boss EventLoopGroup中的一個Eventloop;
- Worker EventLoopGroup中的一個Eventloop,可以監聽多個客戶端Socket;
EventLoop
一個EventLoop其實和一個特定的線程綁定, 並且在其生命週期內, 綁定的線程都不會再改。
EventLoop肩負着兩種任務:
- 第一個是作爲 IO 線程, 執行與 Channel 相關的 IO 操作, 包括 調用select等待就緒的IO事件、讀寫數據與數據的處理等;
- 第二個任務是作爲任務隊列, 執行 taskQueue 中的任務, 例如用戶調用eventLoop.schedule提交的定時任務也是這個線程執行的;
第一個任務比較好理解,主要解釋下第二個:從socket數據到數據處理,再到寫入響應數據,Netty都在一個線程中處理,主要是爲了線程安全考慮,減少競爭和線程切換,通過任務隊列的方式,可以在用戶線程提交處理邏輯,在Eventloop中執行。
整個EventLoop乾的事情就是select -> processIO -> runAllTask,processIO處理IO事件相關的邏輯,runAllTask處理任務隊列中的任務,如果執行的任務過多,會影響IO事件的處理,所以會限制任務處理的時間,整個處理過程如下圖:
EventLoop的run代碼如下:
protected void run() {
for (; ; ) {
oldWakenUp = wakenUp.getAndSet(false);
try {
if (hasTasks()) { //如果有任務,快速返回
selectNow();
} else {
select(); //如果沒任務,等待事件返回
if (wakenUp.get()) {
selector.wakeup();
}
}
cancelledKeys = 0;
final long ioStartTime = System.nanoTime();
needsToSelectAgain = false;
//處理IO事件
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
//計算IO處理時間
final long ioTime = System.nanoTime() - ioStartTime;
final int ioRatio = this.ioRatio; //默認爲50
//處理提交的任務
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
ChannelPipeline和ChannelHandler
ChannelPipeline是一個接口,其有一個默認的實現類DefaultChannelPipeline,內部有兩個屬性:head和tail,
這兩者都實現了ChannelHandler接口,對應處理鏈的頭和尾。
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
每個Channel創建時,會創建一個ChannelPipeline對象,來處理channel的各種事件,可以在運行時動態進行動態修改其中的 ChannelHandler。
ChannelHandler承載業務處理邏輯的地方,我們接觸最多的類,可以自定義Handler,加入處理鏈中,實現自定義邏輯。
ChannelHandler 可分爲兩大類:ChannelInboundHandler 和 ChannelOutboundHandle,這兩接口分別對應入站和出站消息的處理,對應數據讀取和數據寫入。它提供了接口方法供我們實現,處理各種事件。
public interface ChannelInboundHandler extends ChannelHandler {
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
void channelActive(ChannelHandlerContext ctx) throws Exception;
void channelInactive(ChannelHandlerContext ctx) throws Exception;
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
}
自定義Handler時,一般繼承ChannelInboundHandlerAdapter或 ChannelOutboundHandlerAdapter。
需要注意的是,不建議在 ChannelHandler 中直接實現耗時或阻塞的操作,因爲這可能會阻塞 Netty 工作線程,導致 Netty 無法及時響應 IO 處理。
使用Netty實現Websocket協議
Websocket協議
不是本篇的重點,簡單說明下:
- 是一種長連接協議,大部分瀏覽器都支持,通過websocket,服務端可以主動發消息給客戶端;
- Websocket協議,在握手階段使用HTTP協議,握手完成之後,走Websocket自己的協議;
- Websocket是一種二進制協議;
初始化
Netty提供了ChannelInitializer類方便我們初始化,創建WebSocketServerInitializer類,繼承ChannelInitializer類,用於添加ChannelHandler:
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
@Resource
private CustomTextFrameHandler customTextFrameHandler;
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("codec-http", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
pipeline.addLast("websocket-protocal-handler",new WebSocketServerProtocolHandler());
pipeline.addLast("custome-handler", customTextFrameHandler);
}
}
分析下這幾個Handler,都是Netty默認提供的:
- HttpServerCodec:用於解析Http請求,主要在握手階段進行處理;
- HttpObjectAggregator:用於合併Http請求頭和請求體,主要在握手階段進行處理;
- WebSocketServerProtocolHandler:處理Websocket協議;
- CustomTextFrameHandler:自定義的Handler,用於添加自己的業務邏輯。
是不是很方便,經過WebSocketServerProtocolHandler處理後,讀取出來的就是文本數據了,不用自己處理數據合包、拆包問題。
CustomTextFrameHandler
自定義的Handler,進行業務處理:
public class CustomTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(final ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
final String content = frame.text();
System.out.println("接收到數據:"+content);
// 回覆數據
TextWebSocketFrame respFrame = new TextWebSocketFrame("我收到了你的數據");
if (ctx.channel().isWritable()) {
ChannelFuture future = ctx.writeAndFlush(respFrame);
}
}
}
歡迎掃描下方二維碼,關注我的個人微信公衆號 ~