netty系列之:文本聊天室


簡介

經過之前的系列文章,我們已經知道了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/

最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM