簡介
經過之前的系列文章,我們已經知道了netty的執行原理,還介紹了基本的netty服務搭建流程和訊息處理器的寫法。今天本文會給大家介紹一個更加複雜的例子,文字聊天室。
聊天室的工作流程
今天要介紹的是文字聊天室,對於文字聊天室來說,首先需要建立一個伺服器,用於處理各個客戶端的連線,對於客戶端來說,需要建立和伺服器的連線,然後向伺服器輸入聊天資訊。伺服器收到聊天資訊之後,會對訊息進行響應,並將訊息返回至客戶端,這樣一個聊天室的流程就完成了。
文字處理器
之前的文章中,我們有提到過,netty的傳輸只支援ByteBuf型別,對於聊天室直接輸入的字串是不支援的,需要對字串進行encode和decode轉換。
之前我們介紹的encode和decode的類叫做ObjectDecoder和ObjectEncoder。今天我們再介紹兩個專門處理字串的StringDecoder和StringEncoder。
StringEncoder要比ObjectEncoder簡單很多,因為對於物件來說,我們還需要在Byte陣列的頭部設定Byte陣列的大小,從而保證物件所有資料讀取正確。對於String來說,就比較簡單了,只需要保證一次讀入的資料都是字串即可。
StringEncoder繼承自MessageToMessageEncoder,其核心的encode程式碼如下:
protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
if (msg.length() == 0) {
return;
}
out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset));
}
從上面的程式碼可以看出,核心實際上是呼叫了ByteBufUtil.encodeString方法,將String轉換成了ByteBuf。
對於字串編碼來說,還需要界定一個編碼的範圍,比如我們需要知道需要一次編碼多少字串,一般來說我們通過回車符來界定一次字串輸入的結束。
netty也提供了這樣的非常便利的類叫做DelimiterBasedFrameDecoder,通過傳入不同的Delimiter,我們可以將輸入拆分成不同的Frame,從而對一行字串進行處理。
new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
我再看一下StringDecoder的核心程式碼,StringDecoder繼承自MessageToMessageDecoder:
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
out.add(msg.toString(charset));
}
通過呼叫ByteBuf的toString方法,將BuyteBuf轉換成為字串,並且輸出到channel中。
初始化ChannelHandler
在initChannel的時候,我們需要向ChannelPipeline中新增有效的Handler。對於本例來說,需要新增StringDecoder、StringEncoder、DelimiterBasedFrameDecoder和真正處理訊息的自定義handler。
我們將初始化Pipeline的操作都放在一個新的ChatServerInitializer類中,這個類繼承自ChannelInitializer,其核心的initChannel方法如下:
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 新增行分割器
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 新增String Decoder和String Encoder,用來進行字串的轉換
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
// 最後新增真正的處理器
pipeline.addLast(SERVER_HANDLER);
}
ChatServerInitializer在Bootstrap中的childHandler中進行新增:
childHandler(new ChatServerInitializer())
真正的訊息處理邏輯
有了上面的邏輯之後,我們最後只需要專注於真正的訊息處理邏輯即可。
這裡我們的邏輯是當客戶端輸入“再見”的時候,就關閉channel,否則就將訊息回寫給客戶端。
其核心邏輯如下:
public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {
// 如果讀取到"再見"就關閉channel
String response;
// 判斷是否關閉
boolean close = false;
if (request.isEmpty()) {
response = "你說啥?\r\n";
} else if ("再見".equalsIgnoreCase(request)) {
response = "再見,我的朋友!\r\n";
close = true;
} else {
response = "你是不是說: '" + request + "'?\r\n";
}
// 寫入訊息
ChannelFuture future = ctx.write(response);
// 新增CLOSE listener,用來關閉channel
if (close) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
通過判斷客戶端的出入,來設定是否關閉按鈕,這裡的關閉channel是通過向ChannelFuture中新增ChannelFutureListener.CLOSE來實現的。
ChannelFutureListener.CLOSE是一個ChannelFutureListener,它會在channel執行完畢之後關閉channel,事實上這是一個非常優雅的關閉方式。
ChannelFutureListener CLOSE = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
future.channel().close();
}
};
對於客戶端來說,其核心就是從命令列讀取輸入,這裡使用InputStreamReader接收命令列輸入,並使用BufferedReader對其快取。
然後將命令列輸入通過呼叫 ch.writeAndFlush寫入到channel中,最後監聽命令列輸入,如果監聽到“再見“,則等待server端關閉channel,其核心程式碼如下。
// 從命令列輸入
ChannelFuture lastWriteFuture = null;
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
break;
}
// 將從命令列輸入的一行字元寫到channel中
lastWriteFuture = ch.writeAndFlush(line + "\r\n");
// 如果輸入'再見',則等待server端關閉channel
if ("再見".equalsIgnoreCase(line)) {
ch.closeFuture().sync();
break;
}
}
// 等待所有的訊息都寫入channel中
if (lastWriteFuture != null) {
lastWriteFuture.sync();
}
總結
經過上面的介紹,一個簡單的聊天室就建成了。後續我們會繼續探索更加複雜的應用,希望大家能夠喜歡。
本文的例子可以參考:learn-netty4
本文已收錄於 http://www.flydean.com/10-netty-chat/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!