spring boot 实现直播聊天室(二)

spring boot 实现直播聊天室(二)

技术方案:

  • spring boot
  • netty
  • rabbitmq

目录结构

在这里插入图片描述

引入依赖

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

SimpleNettyWebsocketServer

netty server 启动类

@Slf4j
public class SimpleNettyWebsocketServer {

    private SimpleWsHandler simpleWsHandler;

    public SimpleNettyWebsocketServer(SimpleWsHandler simpleWsHandler) {
        this.simpleWsHandler = simpleWsHandler;
    }

    public void start(int port) throws InterruptedException {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup work = new NioEventLoopGroup(2);
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, work).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    //HTTP协议编解码器,用于处理HTTP请求和响应的编码和解码。其主要作用是将HTTP请求和响应消息转换为Netty的ByteBuf对象,并将其传递到下一个处理器进行处理。
                    pipeline.addLast(new HttpServerCodec());
                    //用于HTTP服务端,将来自客户端的HTTP请求和响应消息聚合成一个完整的消息,以便后续的处理。
                    pipeline.addLast(new HttpObjectAggregator(65535));
                    pipeline.addLast(new IdleStateHandler(30,0,0));
                    //处理请求参数
                    pipeline.addLast(new SimpleWsHttpHandler());
                    pipeline.addLast(new WebSocketServerProtocolHandler("/n/ws"));
                    pipeline.addLast(simpleWsHandler);

                }
            });
            Channel channel = bootstrap.bind(port).sync().channel();
            log.info("server start at port: {}", port);
            channel.closeFuture().sync();
        } finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }
}

NettyUtil: 工具类

public class NettyUtil {

    public static AttributeKey<String> G_U = AttributeKey.valueOf("GU");

    /**
     * 设置上下文参数
     * @param channel
     * @param attributeKey
     * @param data
     * @param <T>
     */
    public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {
        Attribute<T> attr = channel.attr(attributeKey);
        if (attr != null) {
            attr.set(data);
        }
    }


    /**
     * 获取上下文参数 
     * @param channel
     * @param attributeKey
     * @return
     * @param <T>
     */
    public static <T> T getAttr(Channel channel, AttributeKey<T> attributeKey) {
        return channel.attr(attributeKey).get();
    }


    /**
     * 根据 渠道获取 session
     * @param channel
     * @return
     */
    public static NettySimpleSession getSession(Channel channel) {
        String attr = channel.attr(G_U).get();
        if (StrUtil.isNotBlank(attr)){
            String[] split = attr.split(",");
            String groupId = split[0];
            String username = split[1];
            return new NettySimpleSession(channel.id().toString(),groupId,username,channel);
        }
        return null;
    }
}

处理handler

SimpleWsHttpHandler

处理 websocket 协议升级时地址请求参数 ws://127.0.0.1:8881/n/ws?groupId=1&username=tom, 解析groupId 和 username ,并设置这个属性到上下文

/**
 * @Date: 2023/12/13 9:53
 * 提取参数
 */
@Slf4j
public class SimpleWsHttpHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof  FullHttpRequest request){
            //ws://localhost:8080/n/ws?groupId=xx&username=tom
            String decode = URLDecoder.decode(request.uri(), StandardCharsets.UTF_8);
            log.info("raw request url: {}", decode);
            Map<String, String> queryMap = getParams(decode);
            String groupId = MapUtil.getStr(queryMap, "groupId", null);
            String username = MapUtil.getStr(queryMap, "username", null);
            if (StrUtil.isNotBlank(groupId) && StrUtil.isNotBlank(username)) {
                NettyUtil.setAttr(ctx.channel(), NettyUtil.G_U, groupId.concat(",").concat(username));
            }
            //去掉参数 ===>  ws://localhost:8080/n/ws
            request.setUri(request.uri().substring(0,request.uri().indexOf("?")));
            ctx.pipeline().remove(this);
            ctx.fireChannelRead(request);
        }else{
            ctx.fireChannelRead(msg);
        }
    }

    /**
     * 解析 queryString
     * @param uri
     * @return
     */
    public static Map<String, String> getParams(String uri) {
        Map<String, String> params = new HashMap<>(10);
        int idx = uri.indexOf("?");
        if (idx != -1) {
            String[] paramsArr = uri.substring(idx + 1).split("&");
            for (String param : paramsArr) {
                idx = param.indexOf("=");
                params.put(param.substring(0, idx), param.substring(idx + 1));
            }
        }

        return params;
    }
}
SimpleWsHandler

处理消息

@Slf4j
@ChannelHandler.Sharable
public class SimpleWsHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Autowired
    private PushService pushService;

    /**
     * 在新的 Channel 被添加到 ChannelPipeline 中时被调用。这通常发生在连接建立时,即 Channel 已经被成功绑定并注册到 EventLoop 中。
     * 在连接建立时被调用一次
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        NettySimpleSession session = NettyUtil.getSession(ctx.channel());
        if (session == null) {
            log.info("handlerAdded channel id: {}", ctx.channel().id());
        } else {
            log.info("handlerAdded channel group-username: {}-{}", session.group(), session.identity());
        }
    }

    /**
     * 连接断开时,Netty 会自动触发 channelInactive 事件,并将该事件交给事件处理器进行处理
     * 在 channelInactive 事件的处理过程中,会调用 handlerRemoved 方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        NettySimpleSession session = NettyUtil.getSession(ctx.channel());
        if (session!=null){
            log.info("handlerRemoved channel group-username: {}-{}", session.group(), session.identity());
        }
        offline(ctx.channel());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        //todo msg 可以是json字符串,这里仅仅只是纯文本
        NettySimpleSession session = NettyUtil.getSession(ctx.channel());
        if (session!=null){
            MessageDto messageDto = new MessageDto();
            messageDto.setSessionId(session.getId());
            messageDto.setGroup(session.group());
            messageDto.setFromUser(session.identity());
            messageDto.setContent(msg.text());
            pushService.pushGroupMessage(messageDto);
        }else {
            log.info("channelRead0 session is null channel id: {}-{}", ctx.channel().id(),msg.text());
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("SimpleWsHandler 客户端异常断开 {}", cause.getMessage());
        //todo offline
        offline(ctx.channel());
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent idleStateEvent) {
            if (idleStateEvent.state().equals(IdleStateEvent.READER_IDLE_STATE_EVENT)) {
                log.info("SimpleWsIdleHandler channelIdle 5 秒未收到客户端消息,强制关闭: {}", ctx.channel().id());
                //todo offline
                offline(ctx.channel());
            }
        } else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            String attr = NettyUtil.getAttr(ctx.channel(), NettyUtil.G_U);
            if (StrUtil.isBlank(attr)) {
                ctx.writeAndFlush("参数异常");
                offline(ctx.channel());
            } else {
                //todo 可以做用户认证等等
                //记录用户登陆session
                NettySimpleSession session = NettyUtil.getSession(ctx.channel());
                Assert.notNull(session, "session 不能为空");
                SessionRegistry.getInstance().addSession(session);
            }
        }
        super.userEventTriggered(ctx,evt);
    }

    /**
     * 用户下线,处理失效 session
     * @param channel
     */
    public void offline(Channel channel){
        NettySimpleSession session = NettyUtil.getSession(channel);
        if (session!=null){
            SessionRegistry.getInstance().removeSession(session);
        }
        channel.close();
    }

}

PushService

推送服务抽取

public interface PushService {

    /**
     * 组推送
     * @param messageDto
     */
    void pushGroupMessage(MessageDto messageDto);

}

@Service
public class PushServiceImpl implements PushService {

    @Autowired
    private MessageClient messagingClient;

    @Override
    public void pushGroupMessage(MessageDto messageDto) {
        messagingClient.sendMessage(messageDto);
    }
}

NettySimpleSession

netty session 封装

public class NettySimpleSession extends AbstractWsSession {

    private Channel channel;

    public NettySimpleSession(String id, String group, String identity, Channel channel) {
        super(id, group, identity);
        this.channel = channel;
    }

    @Override
    public void sendTextMessage(MessageDto messageDto) {
        String content = messageDto.getFromUser() + " say: " + messageDto.getContent();
        // 不能直接 write content, channel.writeAndFlush(content);
        // 要封装成 websocketFrame,不然不能编解码!!!
        channel.writeAndFlush(new TextWebSocketFrame(content));
    }
}

启动类

@Slf4j
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }


    @Bean
    public SimpleWsHandler simpleWsHandler(){
        return new SimpleWsHandler();
    }

    @PostConstruct
    public void init() {
        new Thread(() -> {
            log.info(">>>>>>>> start netty ws server....");
            try {
                new SimpleNettyWebsocketServer(simpleWsHandler()).start(8881);
            } catch (InterruptedException e) {
                log.info(">>>>>>>> SimpleNettyWebsocketServer start error", e);
            }
        }).start();
    }

}

其他代码参考 spring boot 实现直播聊天室

测试

websocket 地址 ws://127.0.0.1:8881/n/ws?groupId=1&username=tom

在这里插入图片描述

good luck!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一种用于构建企业级的Java应用程序的框架,能够让开发者更加便捷地开发高效的应用程序。而Vue则是一种现代化的、渐进式的JavaScript框架,被广泛应用于前端开发。当将这两种框架结合起来,可以实现一个简单的聊天室程序,使得用户可以方便地进行在线交流。 Spring Boot中可以通过WebSockets实现实时通信,而Vue则可以通过组件化的方式快速构建出UI的交互效果。具体来说,可以在Vue中创建一个聊天室页面,用户可以在该页面中发送消息,而这些消息可以通过WebSockets实时地传输到后台Spring Boot应用程序中,后台的程序将这些消息进行处理,并将处理结果再次透过WebSockets发送回前端,更新聊天室页面上的显示。 这个简单的聊天室程序可以实现以下功能: 1. 用户可以通过该聊天室程序在在线状态下进行交流。 2. 用户可以发送消息,并在聊天室页面上实时查看收到的消息。 3. 用户可以在聊天室页面中查看历史消息。 4. 群聊或者私聊功能可自行添加。 总之,Spring Boot和Vue的组合可以实现一个简单而实用的聊天室程序,可以在不同场合下方便地应用。此外,这只是其中的一个简单应用场景,这两种框架的结合还可以实现其他许多应用,开发效率也非常高。因此,对于企业级的应用程序开发来说,这种框架结合的方式是非常值得探索的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值