服务端使用netty的步骤

  1. 在服务端,需要使用NioEventLoopGroup创建两个 NIO 线程组。NioEventLoopGroup是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。
    bossGroup线程组:Boss线程,由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。
    workerGroup线程组:Worker线程,Worker线程执行所有的异步I/O,即处理操作。
    (在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。)
  2. 创建ServerBootstrap对象,这是一个启动NIO服务的辅助启动类,负责初始化netty服务器,并且开始监听端口的socket请求,用于配置Netty的一系列参数,如接受传出数据的缓存大小等。
  3. 创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口
  4. 绑定端口,执行同步阻塞方法等待服务器端启动即可。

使用netty实现一个简易聊天服务器

该实例参考了这篇文章:Netty 实现聊天功能

1. SimpleChatServer.java

public class SimpleChatServer {
    private int port;

    public SimpleChatServer(int port) {
        this.port = port;
    }

    public void run() throws InterruptedException {
        // 创建两个线程组,Boss和Worker
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建ServerBootstrap实例,用于配置netty的一系列参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 绑定两个线程组
            bootstrap.group(bossGroup, workerGroup)
                    // 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
                    .channel(NioServerSocketChannel.class)

                    // SimpleChatServerInitializer, 继承于ChannelInitializer,目的是配置一个新的 Channel
                    .childHandler(new SimpleChatServerInitializer())

                    // option() 是提供给NioServerSocketChannel 用来接收进来的连接。
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这里也是NioServerSocketChannel
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            System.out.println("SimpleChatServer 启动了");

            // 绑定端口,开始接收进来的连接
            // 可以多次调用 bind() 方法(基于不同绑定地址)
            Channel ch = bootstrap.bind(port).sync().channel();

            // 等待服务器 socket关闭
            // future.channel().closeFuture().sync();
            ch.closeFuture().sync();

        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("SimpleChatServer 关闭了");
        }
    }
}

2. SimpleChatServerInitializer.java

public class SimpleChatServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // netty中解决TCP的粘包拆包问题,这里使用自定义分隔符的方法,使用分隔符类DelimiterBasedFrameDecoder
        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));

        // 编码解码
        pipeline.addLast("decoder", new StringDecoder());
        pipeline.addLast("encoder", new StringEncoder());

        // 自定义的处理实际业务逻辑的handler
        pipeline.addLast("headler", new SimpleChatServerHandler());

        System.out.println("SimpleChatClient:" + socketChannel.remoteAddress() + "连接上");

    }
}

3. SimpleChatServerHandler.java

public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> {

    /**
     * 一个存储类,本质是一个Set
     */
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 每当从服务端收到新的客户端连接时,客户端的 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("[SERVER] - " + incoming.remoteAddress() + "加入\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("[SERVER] - " + incoming.remoteAddress() + "离开\n");
        }
        channels.remove(ctx.channel());
    }


    /**
     *
     * 每当从服务端读到客户端写入信息时,将信息转发给其他客户端的 Channel。
     * 其中如果你使用的是 Netty 5.x 以下版本时,需要把 messageReceived() 重命名为 channelRead0()
     * 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,也就是服务端接收客户端发来的数据。
     * 但是这个数据在不进行解码时它是ByteBuf类型的
     * @param ctx
     * @param s
     * @throws Exception
     */
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, String s) throws Exception {
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            if (channel != incoming) {
                channel.writeAndFlush("[" + incoming.remoteAddress() + "]" + s + "\n");
            } else {
                channel.writeAndFlush("[you]" + s + "\n");
            }
        }
    }


    /**
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端已经建立了通信通道并且可以传输数据
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "在线");
    }

    /**
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "掉线");

    }

    /**
     * 当出现 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("SimpleChatClient:" + incoming.remoteAddress() + "异常");
        // 当出现异常就关闭连接
        cause.printStackTrace();
        ctx.close();
    }

}

最后创建一个Main类,创建一个main方法来运行该服务器:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new SimpleChatServer(port).run();
    }
}

(注意:这里仅仅是服务器的开发,并没有编写客户端。)