SpringBoot+Netty 300行代码实现一个简易版的微信群聊功能

最近有同学在面试中被问到,如何用Netty实现群聊系统,其实这道面试题并不难,只要学习过Netty的同学,应该都能说出个1,2,3,4来。

但是面试官却要他现场编码出来,听到这个要求,我不禁感叹道:“现在的面试都这么卷的了吗”。

所以小北就趁着这个机会,给大家一起分享下该如何用Netty设计完成一个简易版的微信群聊系统。

一、什么是Netty?

在当今的软件开发中,我们经常依赖通用的应用程序或库来实现数据交互。

比如,使用HTTP客户端库从Web服务器获取数据,或者通过Web服务执行远程调用,这些都是常见的做法。

但是,通用协议并不总是能满足所有需求。

想象一下,如果需要处理大文件传输、电子邮件、实时金融信息或多人游戏数据,标准的HTTP服务器就显得力不从心了。

这时,我们需要定制化的解决方案,比如为聊天应用优化的Ajax技术、媒体流传输或大文件传输器。甚至,设计一个全新的协议来精确满足特定需求也不失为一种选择。

此外,与旧系统的兼容性也是一个挑战。面对必须支持的遗留专有协议,关键在于如何迅速实现它们,同时确保应用的稳定性和性能不受影响。

这时候,Netty就应运而生了。

Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能、可扩展协议的服务器和客户端。

说人话就是,A、B、C想要进行网络通信,使用Netty可以快速简单地开发网络应用程序,比如服务器和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发,最重要的是性能很高

二、什么是群聊系统

想要开发一个群聊系统,首先就要知道一个群聊系统要具备什么功能。

2.1 简易版群聊系统功能

1、一人发送,群聊中在线的用户全部能收到信息

2、用户上线、离线、在线、异常掉线提醒

项目搭建

用Springboot主要是为了快速搭建一个可运行的项目,其实不用SpringBoot也可以。

新建一个Springboot项目就不详细演示了,相信这个对于大家还是小菜一碟的

项目搭建好的结构如下

引入Netty依赖

在Springboot的pom文件中引入Netty4.x依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.112.Final</version>
</dependency>

服务端实现

让我们从 handler (处理器)的实现开始,handler 是由 Netty 生成用来处理 I/O 事件的,是整个通信过程中的核心。

SimpleChatServerHandler.java

public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> {
    // 定义一个ChannelGroup来保存所有连接的Channel
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        Channel incoming = ctx.channel();
        // 当handler被添加到pipeline时,发送加入消息给所有Channel
        channels.writeAndFlush("【Server】 - " + incoming.read() + "加入\n");
        // 将当前Channel添加到ChannelGroup中
        channels.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        Channel incoming = ctx.channel();
        // 当handler从pipeline移除时,发送离开消息给所有Channel
        channels.writeAndFlush("【Server】 - " + incoming.read() + "离开\n");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel incoming = ctx.channel();
        // 遍历ChannelGroup中的所有Channel
        for (Channel channel : channels) {
            if (channel != incoming) {
                // 如果不是当前Channel,将消息发送给其他Channel,显示为来自incoming Channel
                channel.writeAndFlush("【" + incoming.remoteAddress() + "】" + msg + "\n");
            } else {
                // 如果是当前Channel,标记消息为"you"
                channel.writeAndFlush("【you】" + msg + "\n");
            }
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Channel incoming = ctx.channel();
        // 当Channel活动时,打印在线信息
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "在线");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        Channel incoming = ctx.channel();
        // 当Channel非活动时,打印掉线信息
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "掉线");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Channel incoming = ctx.channel();
        // 捕获异常,打印异常信息,并关闭Channel
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "异常");
        cause.printStackTrace();
        ctx.close();
    }
}

1、SimpleChatServerHandler 继承自 SimpleChannelInboundHandler,这个类实现了 ChannelInboundHandler 接口,ChannelInboundHandler 提供了许多事件处理的接口方法,然后你可以覆盖这些方法。

现在仅仅只需要继承 SimpleChannelInboundHandler 类而不是你自己去实现接口方法。

2、覆盖了 handlerAdded() 事件处理方法。

每当从服务端收到新的客户端连接时,客户端的 Channel 存入 ChannelGroup 列表中,并通知列表中的其他客户端 Channel

3、覆盖了 handlerRemoved() 事件处理方法。

每当从服务端收到客户端断开时,客户端的 Channel 自动从 ChannelGroup 列表中移除了,并通知列表中的其他客户端 Channel

4、覆盖了 channelRead0() 事件处理方法。

每当从服务端读到客户端写入信息时,将信息转发给其他客户端的 Channel。其中如果你使用的是 Netty 5.x 版本时,需要把 channelRead0() 重命名为messageReceived()

5、覆盖了 channelActive() 事件处理方法。服务端监听到客户端活动

6、覆盖了 channelInactive() 事件处理方法。服务端监听到客户端不活动

7、exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。

在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。

SimpleChatServerInitializer.java

public class SimpleChatServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // 获取Channel的pipeline
        ChannelPipeline pipeline = socketChannel.pipeline();
        
        // 添加一个基于分隔符的帧解码器,这里使用换行符作为消息的分隔符,最大帧长度为8192字节
        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        
        // 添加一个字符串解码器,将字节解码为字符串
        pipeline.addLast("decoder", new StringDecoder());
        
        // 添加一个字符串编码器,将字符串编码为字节
        pipeline.addLast("encoder", new StringEncoder());
        
        // 添加自定义的服务器处理器到pipeline
        pipeline.addLast("handler", new SimpleChatServerHandler());
        
        // 打印客户端连接信息
        System.out.println("SimpleChatClient:" + socketChannel.remoteAddress() + "连接上了");
    }
}

ChannelInitializer是一个用于初始化新创建的Channel的类。在这里,它被用来设置几个自定义的处理器和编解码器:

1、DelimiterBasedFrameDecoder:这个解码器根据指定的分隔符(这里是换行符)来确定消息的边界。

这对于处理基于文本的协议很有用。

2、StringDecoder:将接收到的字节解码成字符串,以便后续处理。

3、StringEncoder:将字符串编码为字节,以便发送到网络。

4、SimpleChatServerHandler:这是自定义的服务器处理器,负责处理接收到的消息和事件。

5、最后,当一个新的SocketChannel被接受时,initChannel方法会被调用,它会设置好pipeline,然后打印出连接上的消息。

SimpleChatServer.java(服务端启动类)

编写一个 main() 方法来启动服务端。

public class SimpleChatServer {
    private int port; // 服务器端口号

    // 构造函数,传入服务器端口号
    public SimpleChatServer(int port) {
        this.port = port;
    }

    // 运行服务器的方法
    public void run() throws Exception {
        // 创建两个EventLoopGroup,分别用于接受连接和处理连接
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        
        try {
            // 创建ServerBootstrap实例,用于启动服务器
            ServerBootstrap bootstrap = new ServerBootstrap();
            
            // 设置bossGroup和workGroup
            bootstrap.group(bossGroup, workGroup)
                    // 设置用于接受连接的服务器SocketChannel的实现类
                    .channel(NioServerSocketChannel.class)
                    // 设置用于初始化新创建的Channel的ChannelInitializer
                    .childHandler(new SimpleChatServerInitializer())
                    // 设置服务器SocketOption参数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 设置新创建的子Channel的SocketOption参数
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            
            // 打印服务器启动信息
            System.out.println("SimpleChatServer启动了");

            // 绑定服务器端口并启动服务器
            ChannelFuture channelFuture = bootstrap.bind(port).sync();
            
            // 等待直到服务器Socket关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            // 优雅关闭EventLoopGroup
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
            // 打印服务器关闭信息
            System.out.println("SimpleChatServer 关闭了");
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        // 从命令行参数获取端口号,如果没有提供则使用默认端口8080
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        // 创建服务器实例并启动
        new SimpleChatServer(port).run();
    }
}
  1. NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
  2. ServerBootstrap 是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
  3. 这里我们指定使用 NioServerSocketChannel 类来举例说明一个新的 Channel 如何接收进来的连接。
  4. 这里的事件处理类经常会被用来处理一个最近的已经接收的 Channel。SimpleChatServerInitializer 继承自ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。也许你想通过增加一些处理类比如 SimpleChatServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline 来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
  5. 你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如tcpNoDelay 和 keepAlive。请参考 ChannelOption 和详细的 ChannelConfig 实现的接口文档以此可以对ChannelOption 的有一个大概的认识。
  6. option() 是提供给NioServerSocketChannel 用来接收进来的连接。childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这个例子中也是 NioServerSocketChannel。
  7. 我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用 bind() 方法(基于不同绑定地址)。

恭喜!你已经完成了基于 Netty 聊天服务端程序。

客户端实现

SimpleChatClientHandler.java

客户端的处理器就比较简单了,我们直接在控制台打印出来消息就可以了。

public class SimpleChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(s);
    }
}

SimpleChatClientInitializer.java

与服务端类似,我们直接拷贝服务端代码改一下处理器即可

public class SimpleChatClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel 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 SimpleChatClientHandler());
    }
}

SimpleChatClient.java

编写一个 main() 方法来启动客户端。

public class SimpleChatClient {
    private final String host; // 服务器的主机名或IP地址
    private final int port;    // 服务器端口号

    // 构造函数,传入服务器的主机名或IP地址和端口号
    public SimpleChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    // 运行客户端的方法
    public void run() throws Exception {
        // 创建EventLoopGroup,用于处理事件循环
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建Bootstrap实例,用于启动客户端
            Bootstrap bootstrap = new Bootstrap();

            // 设置Bootstrap参数
            bootstrap.group(group)
                    // 设置用于创建连接的SocketChannel的实现类
                    .channel(NioSocketChannel.class)
                    // 设置用于初始化Channel的ChannelInitializer
                    .handler(new SimpleChatClientInitializer());

            // 连接到服务器,并获取Channel对象
            Channel channel = bootstrap.connect(host, port).sync().channel();

            // 创建BufferedReader,用于读取控制台输入
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

            // 循环读取控制台输入并发送到服务器
            while (true) {
                channel.writeAndFlush(in.readLine() + "\r\n");
            }
        } finally {
            // 优雅关闭EventLoopGroup
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        // 创建客户端实例并连接到本地服务器的8080端口
        new SimpleChatClient("localhost", 8080).run();
    }
}

运行效果

先运行 SimpleChatServer,再可以运行多个 SimpleChatClient,控制台输入文本继续测试

运行结果如下:

服务端

客户端1

客户端2

客户端3

总结

作为一个群聊系统,其实现在这个版本非常简陋,比如我们只能发送文本,也没有好看的的UI界面交互。如果感兴趣的小伙伴可以继续钻研完善。

本文的主要目标是通过一个简单的demo带大家感受一下如何用Netty快速简单的搭建一个网络通讯。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值