4. Netty系列——实战(简单聊天室)

360 阅读6分钟

Server端

  1. NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。

io.netty.channel Interface EventLoopGroup

  • EpollEventLoopGroup
  • LocalEventLoopGroup
  • MultithreadEventLoopGroup
  • NioEventLoop
  • NioEventLoopGroup
  • OioEventLoopGroup
  • SingleThreadEventLoop
  • ThreadPerChannelEventLoop
  • ThreadPerChannelEventLoopGroup
// 第一步,创建事件循环器
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
  1. ServerBootstrap 是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做
ServerBootstrap serverBootstrap = new ServerBootstrap();
  1. 设置启动器的group
serverBootstrap.group(bossGroup, workerGroup)
  1. 指定channel
serverBootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class) 
  1. 设置handler
.childHandler(new ChatInitializer())
  1. 设置channel参数 ChannelOption channelConfig
// option() 是提供给NioServerSocketChannel 用来接收进来的连接
.option(ChannelOption.SO_BACKLOG, 128)
  1. 设置childOption参数
// childOption() 是提供给由父管道 ServerChannel 接收到的连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
  1. 绑定端口,开始接收进来的连接
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
  1. 等待服务器socket关闭
channelFuture.channel().closeFuture().sync();
  1. 优雅的关闭channel
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

完整代码

/**
 * Server启动类
 */
public class ChatServer {
    private int port;

    ChatServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new ChatServer(port).start();
    }

    private void start() {
        // 1 定义两个事件循环器,boss用于接收连接,worker用于处理连接,
        // 一旦boss接收到连接,就会吧信息注册到worker上
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 2. ServerBootstrap 是一个启动 NIO 服务的辅助启动类。
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)                  // 指定使用的channel为NIO
                    .localAddress(new InetSocketAddress(port))              // 设置socket地址锁使用的端口
                    .childHandler(new ChatInitializer())
                    .option(ChannelOption.SO_BACKLOG, 128)             // 指定channel实现的配置参数
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 绑定端口,开始接收进来的连接
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            // 等待服务器socket关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅的关闭服务器
            // 当EventLoopGroup 被完全地终止,并且对应的所有 channel 都已经被关闭时,
            // Netty 会返回一个Future对象来通知你
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}


/**
 * 用来增加多个的处理类到 ChannelPipeline 上,包括编码、解码、Handler 等
 *
 * ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。
 * 也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel
 * 或者其对应的ChannelPipeline 来实现你的网络程序。当你的程序变的复杂时,
 * 可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上
 */
public class ChatInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // 通过socketChannel获取对应的管道
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        // 通过管道添加handler
        // HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器
        // 当请求到服务端,我们需要做解码,响应到客户端做编码
        // WebSocket基于http协议,所以要有http编解码器
        channelPipeline.addLast("HttpServerCodec", new HttpServerCodec());
        // 对写大数据流的支持
        channelPipeline.addLast(new ChunkedWriteHandler());
        // 对httpMessage进行聚合,聚合成FullRequest活FullResponse, 几乎所有的netty编程,都会用的此handler
        channelPipeline.addLast(new HttpObjectAggregator(1024 * 64));
        // ========================= 以上是用于http协议 =========================

        // webSocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws
        // 本handler会帮你处理一些繁重的复杂的事
        // 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
        // 对于webSocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
        channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        channelPipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        // 编码和解码
        channelPipeline.addLast("decoder", new StringDecoder());
        channelPipeline.addLast("encoder", new StringEncoder());

        // 添加自定义助手类
        channelPipeline.addLast("ChatServerHandler", new ChatHandler());

        System.out.println("server is online...");
    }

/**
 * SimpleChatServerHandler 继承自 SimpleChannelInboundHandler
 * 这个类实现了 ChannelInboundHandler接口,ChannelInboundHandler 提供了许多事件处理的接口方法,
 * 然后你可以覆盖这些方法。现在仅仅只需要继承 ChannelInboundHandlerAdapter 类而不是你自己去实现接口方法
 */
public class ChatHandler extends SimpleChannelInboundHandler {
    // 用于记录和管理所有客户端的channel
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 每当从服务端读到客户端写入信息时,将信息转发给其他客户端的 Channel。
     * 其中如果你使用的是 Netty 5.x 版本时,需要把 channelRead0() 重命名为messageReceived()
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("read");
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            if (channel != incoming) {
                // 打印其他的channel msg
                channel.writeAndFlush("[" + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "]" + msg + "\n");
            } else {
                // 打印自己的channel msg
                channel.writeAndFlush("[you]" + msg + "\n");
            }
        }
        channels.add(ctx.channel());
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("chatClient: " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "注册\n");
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("chatClient: " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "注销\n");
    }

    /**
     * 服务端监听到客户端活动
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("chatClient: " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "在线\n");
    }

    /**
     * 服务端监听到客户端不活动
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("chatClient: " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "在线\n");
    }

    /**
     * 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。
     * 在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式
     * 会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("chatClient: " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "异常\n");
        // 出现异常关闭连接
        // 打印异常信息
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 每当从服务端收到新的客户端连接时,客户端的 Channel 存入ChannelGroup列表中,
     * 并通知列表中的其他客户端 Channel
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush("[client] - " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "加入\n");
        }
        channels.add(ctx.channel());
    }

    /**
     * 每当从服务端收到客户端断开时,客户端的 Channel 移除 ChannelGroup 列表中,
     * 并通知列表中的其他客户端 Channel
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush("[client] - " + incoming.remoteAddress() + " - ID: " + incoming.id().asShortText() + "离开\n");
        }
        channels.add(ctx.channel());
    }
}

client

public class ChatClient1 {
    private String address;
    private int port;

    public ChatClient1(String address, int port) {
        this.address = address;
        this.port = port;
    }

    public static void main(String[] args) {
        new ChatClient1("localhost", 8080).start();
    }

    private void start() {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("handler", new SimpleChannelInboundHandler<String>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                                    System.out.println(msg);
                                }
                            });
                        }
                    });

            Channel channel = bootstrap.connect(address, port).sync().channel();
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

            while (true) {
                System.out.println("sout:" + in.readLine());
                System.out.println(channel.id().asShortText());
                channel.writeAndFlush(in.readLine() + "\r\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }
}