Netty 系列教程(一) 幹擼一個聊天室

爲什麼學習 Netty

在前面已經學習了 SOCKET 和 NIO ,從上幾章也知道,傳統的 NIO 編程,就是一個線程,對應一個selector,客戶端的接入、數據讀寫都在一個線程,這樣導致的後果就是沒利用好CPU,且當接收客戶端阻塞時,數據讀寫是進行不了的。
另外,NIO 的空轉100%cpu佔用率的問題,我們也沒有解決;

筆者曾經對 NIO 進行了擴展 ,比如單獨一個 線程池對應 selector 的 accept 客戶端,另外的兩個線程池,對應 selector 的READ 和 WRITE 操作;雖然,線程數進行了控制,且對 byteBuffer 也進行了擴展和填充,避免了數據黏包的問題,但是在 文件傳輸和要進行其他擴展時,總覺得難以進行,故而學習一下 Netty 是很有必要的。

至於 Netty 是什麼,相信你已經對它進行過了解了,總之就是叼得一逼,例子和輪胎都不錯,可以先看4.x的文檔:
Netty 文檔 基本跟着敲一遍都有一個很好的瞭解。

代碼工程:https://github.com/LillteZheng/SocketDemo

該教程,後面回去研究一下 Netty 的源碼,再根據裏面的思想,對以前的項目進行一個擴展。

一個聊天室

先看效果:
在這裏插入圖片描述
跟以前的做法一樣,就是服務端充當中轉站,把客戶端的信息接收並傳給其他客戶端;
接着,來看看Netty 的服務端的配置和 傳統的 NIO 有什麼不同

public class ChatServer {
    public static void main(String[] args) throws InterruptedException {
        /**
         * NioEventLoopGroup 是用來處理I/O操作的多線程事件循環器
         * boss 可以理解是 selector 的 accept 單獨一個線程
         * worker 可以理解是 selector 的 read 和 write
         */
        final EventLoopGroup bossGroup = new NioEventLoopGroup();
        final EventLoopGroup workerGroup = new NioEventLoopGroup();
       // ServerBootstrap 是一個啓動 NIO 服務的輔助啓動類
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup,workerGroup)
                    //channel 實例化 NioServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                    // 用來處理 handler ,設置連入服務端的 Client 的 SocketChannel 的處理器
                    .childHandler(new ChatServerInitializer())
                    //option 針對NioServerSocketChannel,比如這裏 128 個客戶端之後,纔開始排隊
                    .option(ChannelOption.SO_BACKLOG,128)
                    // childOption 針對childHandler 的handler
                    .childOption(ChannelOption.SO_KEEPALIVE,true);

            //這裏的啓動時異步的,阻塞等待
            ChannelFuture future = b.bind(Constants.PORT).sync();

            future.addListener(new ChannelFutureListener() {
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (channelFuture.isSuccess()){
                        System.out.println("服務端啓動成功");
                    }
                }
            });

            // 等待服務器  socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的服務器。
            future.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

上面的註釋已經很清楚了,需要注意幾個類,比如 EventLoopGroup 對象,可以理解它爲一個線程池,一個用於接收新的客戶單,一個專注於數據讀寫,這樣的好處是充分結合多線程和 selector 的模式,如果你想要深入瞭解,可以搜索 Netty 的 Readctro 模型。
ServerBootstrap 是NIO服務啓動的一個輔助類,一般 NIO 的配置都是比較麻煩的, Netty 這裏通過 Builder 的模式,可以省略很多步驟。
而 channel 和 childHandler 則是配置服務端和接入的 socketchannel 的屬性的。這裏用 ChatServerInitializer 來實現,後面看具體實現。
最後通過 bind 綁定端口並阻塞接收客戶端的接入。
注意 closeFuture 方法,他是 監聽 服務器關閉,不是關閉服務器,而是監聽 關閉。

接着,繼續看 ChatServerInitializer 的代碼:

public class ChatServerInitializer extends ChannelInitializer<SocketChannel> {
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //採用分隔符處理器,處理黏包問題,防止數據過大導致的黏包問題
        pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Delimiters.lineDelimiter()));
        //編碼
        pipeline.addLast(new StringDecoder());
        //解碼
        pipeline.addLast(new StringEncoder());
        //添加處理器,這裏爲邏輯的處理
        pipeline.addLast(new ChatServerHandler());
    }
}

重點看 ChatServerHandler 它爲服務端主要的業務代碼。這裏爲 聊天室:

public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
    //單例
    static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //提示其他客戶端,有新客戶端加入
        group.writeAndFlush("SERVER - "+channel.remoteAddress()+"加入羣聊\n");
        group.add(channel);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //提示其他客戶端,有新客戶端加入
        System.out.println("handlerRemoved");
        group.writeAndFlush("SERVER - "+channel.remoteAddress()+"離開\n");
      //  group.remove(channel);
        // A closed Channel is automatically removed from ChannelGroup,
        // so there is no need to do "channels.remove(ctx.channel());"
    }

    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        Channel clientChannel = channelHandlerContext.channel();
        //打印信息
        for (Channel channel : group) {
            if (channel != clientChannel){
                channel.writeAndFlush("[" + clientChannel.remoteAddress() + "]" + s + "\n");
            }else{
                channel.writeAndFlush("[you]" + s + "\n");
            }
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelInactive");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"異常");
        cause.printStackTrace();
        ctx.close();
    }
}

可以看到這裏繼承的是 SimpleChannelInboundHandler 。當然,它也可以繼承 ChannelInboundHandlerAdapter ,區別是 SimpleChannelInboundHandler 可以通過泛型指定數據類型,且在接收到數據之後,會自動 release ,避免 byteBuffer 被佔用,而 ChannelInboundHandlerAdapter 則不會自動釋放,需要自己 ReferenceCountUtil.release() ;教程都會說,記得回去看官方說明。
這裏因爲都是字符串類型,所以統一用 SimpleChannelInboundHandler ,當然服務端建議採用 ChannelInboundHandlerAdapter ,因爲有多個不同類型的客戶端接入,在客戶端做區分,並做好釋放即可。客戶端的話,可以用SimpleChannelInboundHandler ,畢竟這個也比較單一。

這樣,服務端的代碼就寫好了。

接着看 客戶端的代碼:
很多都是相似的,先看 ChatClient 的代碼:

public class ChatClient {
    public static void main(String[] args) throws InterruptedException, IOException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(bossGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChatClientInitializer());

            //連接服務器
            final ChannelFuture future = bootstrap.connect("localhost", Constants.PORT).sync();

            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                String msg = br.readLine();
                if (msg.equals("bye")){
                    return;
                }
                future.channel().writeAndFlush(msg+"\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
        }
    }
}

基本與 服務端一直,因爲是客戶端,所以只要配置 channel 和 handler 即可。其中 ChatClientInitializer與服務端代碼基本一直,只是業務邏輯那塊,需要換成**ChatClientHandler **:

public class ChatClientHandler extends SimpleChannelInboundHandler<String> {

    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        //收到服務端消息
        System.out.println(s);
    }
}

這樣,就完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章