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 系列教程(一) 干撸一个聊天室_第1张图片
跟以前的做法一样,就是服务端充当中转站,把客户端的信息接收并传给其他客户端;
接着,来看看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 {
    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 {
    //单例
    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 {

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

这样,就完成了。

你可能感兴趣的:(Socket,网络通信)