欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot+Netty+Vue+WebSocket實(shí)現(xiàn)在線聊天

 更新時(shí)間:2025年04月26日 09:21:21   作者:陌路物是人非  
本文主要介紹了SpringBoot+Netty+Vue+WebSocket實(shí)現(xiàn)在線聊天,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

最近想學(xué)學(xué)WebSocket做一個(gè)實(shí)時(shí)通訊的練手項(xiàng)目

主要用到的技術(shù)棧是WebSocket Netty Vue Pinia MySQL SpringBoot,實(shí)現(xiàn)一個(gè)持久化數(shù)據(jù),單一群聊,支持多用戶的聊天界面

下面是實(shí)現(xiàn)的過程

后端

SpringBoot啟動(dòng)的時(shí)候會(huì)占用一個(gè)端口,而Netty也會(huì)占用一個(gè)端口,這兩個(gè)端口不能重復(fù),并且因?yàn)镹etty啟動(dòng)后會(huì)阻塞當(dāng)前線程,因此需要另開一個(gè)線程防止阻塞住SpringBoot

1. 編寫Netty服務(wù)器

個(gè)人認(rèn)為,Netty最關(guān)鍵的就是channel,可以代表一個(gè)客戶端

我在這使用的是@PostConstruct注解,在Bean初始化后調(diào)用里面的方法,新開一個(gè)線程運(yùn)行Netty,因?yàn)橄M鸑etty受Spring管理,所以加上了spring的注解,也可以直接在啟動(dòng)類里注入Netty然后手動(dòng)啟動(dòng)

@Service
public class NettyService {
    private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private EventLoopGroup workGroup = new NioEventLoopGroup();
    @Autowired
    private WebSocketHandler webSocketHandler;
    @Autowired
    private HeartBeatHandler heartBeatHandler;
    @PostConstruct
    public void initNetty() throws BaseException {
        new Thread(()->{
            try {
                start();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
    @PreDestroy
    public void destroy() throws BaseException {
        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }
    @Async
    public void start() throws BaseException {
        try {
            ChannelFuture channelFuture = new ServerBootstrap()
                    .group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                            nioSocketChannel.pipeline()
// http解碼編碼器
                                    .addLast(new HttpServerCodec())
// 處理完整的 HTTP 消息
                    .addLast(new HttpObjectAggregator(64 * 1024))
// 心跳檢測時(shí)長
                                    .addLast(new IdleStateHandler(300, 0, 0, TimeUnit.SECONDS))
// 心跳檢測處理器
                                    .addLast(heartBeatHandler)
// 支持ws協(xié)議(自定義)
                                    .addLast(new WebSocketServerProtocolHandler("/ws",null,true,64*1024,true,true,10000))
// ws請求處理器(自定義)
                                    .addLast(webSocketHandler)
                            ;
                        }
                    }).bind(8081).sync();
            System.out.println("Netty啟動(dòng)成功");
            ChannelFuture future = channelFuture.channel().closeFuture().sync();
        }
        catch (InterruptedException e){
            throw new InterruptedException ();
        }
        finally {
//優(yōu)雅關(guān)閉
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }
}

服務(wù)器類只是指明一些基本信息,包含處理器類,支持的協(xié)議等等,具體的處理邏輯需要再自定義類來實(shí)現(xiàn)

2. 心跳檢測處理器

心跳檢測是指 服務(wù)器無法主動(dòng)確定客戶端的狀態(tài)(用戶可能關(guān)閉了網(wǎng)頁,但是服務(wù)端沒辦法知道),為了確定客戶端是否在線,需要客戶端定時(shí)發(fā)送一條消息,消息內(nèi)容不重要,重要的是發(fā)送消息代表該客戶端仍然在線,當(dāng)客戶端長時(shí)間沒有發(fā)送數(shù)據(jù)時(shí),代表客戶端已經(jīng)下線

package org.example.payroll_management.websocket.netty.handler;

@Component
@ChannelHandler.Sharable
public class HeartBeatHandler extends ChannelDuplexHandler {

    @Autowired
    private ChannelContext channelContext;
    private static final Logger logger =  LoggerFactory.getLogger(HeartBeatHandler.class);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            // 心跳檢測超時(shí)
            IdleStateEvent e = (IdleStateEvent) evt;
            logger.info("心跳檢測超時(shí)");
            if (e.state() == IdleState.READER_IDLE){
                Attribute<Integer> attr = ctx.channel().attr(AttributeKey.valueOf(ctx.channel().id().toString()));
                Integer userId = attr.get();
                // 讀超時(shí),當(dāng)前已經(jīng)下線,主動(dòng)斷開連接
                ChannelContext.removeChannel(userId);
                ctx.close();
            } else if (e.state() == IdleState.WRITER_IDLE){
                ctx.writeAndFlush("心跳檢測");
            }
        }
        super.userEventTriggered(ctx, evt);
    }
}

3. webSocket處理器

當(dāng)客戶端發(fā)送消息,消息的內(nèi)容會(huì)發(fā)送當(dāng)webSocket處理器中,可以對(duì)對(duì)應(yīng)的方法進(jìn)行處理,我這里偷懶了,就做了一個(gè)群組,全部用戶只能在同一群中聊天,不過創(chuàng)建多個(gè)群組,或單對(duì)單聊天也不復(fù)雜,只需要將群組的ID進(jìn)行保存就可以

這里就產(chǎn)生第一個(gè)問題了,就是SpringMVC的攔截器不會(huì)攔截其他端口的請求,解決方法是將token放置到請求參數(shù)中,在userEventTriggered方法中重新進(jìn)行一次token檢驗(yàn)

第二個(gè)問題,我是在攔截器中通過ThreadLocal保存用戶ID,不走攔截器在其他地方拿不到用戶ID,解決方法是,在userEventTriggered方法中重新保存,或者channel中可以保存附件(自身攜帶的數(shù)據(jù)),直接將id保存到附件中

第三個(gè)問題,消息的持久化,當(dāng)用戶重新打開界面時(shí),肯定希望消息仍然存在,鑒于webSocket的實(shí)時(shí)性,數(shù)據(jù)持久化肯定不能在同一個(gè)線程中完成,我在這使用BlockingQueue+線程池完成對(duì)消息的異步保存,或者也可以用mq實(shí)現(xiàn)

不過用的Executors.newSingleThreadExecutor();可能會(huì)產(chǎn)生OOM的問題,后面可以自定義一個(gè)線程池,當(dāng)任務(wù)滿了之后,指定拒絕策略為拋出異常,再通過全局異常捕捉拿到對(duì)應(yīng)的數(shù)據(jù)保存到數(shù)據(jù)庫中,不過俺這種小項(xiàng)目應(yīng)該不會(huì)產(chǎn)生這種問題

第四個(gè)問題,消息內(nèi)容,這個(gè)需要前后端統(tǒng)一一下,確定一下傳輸格式就OK了,然后從JSON中取出數(shù)據(jù)處理

最后就是在線用戶統(tǒng)計(jì),這個(gè)沒什么好說的,里面有對(duì)應(yīng)的方法,當(dāng)退出時(shí),直接把channel踢出去就可以了

package org.example.payroll_management.websocket.netty.handler;

@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Autowired
    private ChannelContext channelContext;
    @Autowired
    private MessageMapper messageMapper;
    @Autowired
    private UserService userService;
    private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);

    private static final BlockingQueue<WebSocketMessageDto> blockingQueue = new ArrayBlockingQueue(1024 * 1024);
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
    // 提交線程
    @PostConstruct
    private void init(){
        EXECUTOR_SERVICE.submit(new MessageHandler());
    }
    private class MessageHandler implements Runnable{
        // 異步保存
        @Override
        public void run() {
            while(true){
                WebSocketMessageDto message = null;
                try {
                    message = blockingQueue.take();
                    logger.info("消息持久化");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Integer success = messageMapper.saveMessage(message);
                if (success < 1){
                    try {
                        throw new BaseException("保存信息失敗");
                    } catch (BaseException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }

    }
    // 當(dāng)讀事件發(fā)生時(shí)(有客戶端發(fā)送消息)
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        Channel channel = channelHandlerContext.channel();
        // 收到的消息
        String text = textWebSocketFrame.text();
        Attribute<Integer> attr = channelHandlerContext.channel().attr(AttributeKey.valueOf(channelHandlerContext.channel().id().toString()));
        Integer userId = attr.get();
        logger.info("接收到用戶ID為 {} 的消息: {}",userId,text);
        // TODO  將text轉(zhuǎn)成JSON,提取里面的數(shù)據(jù)
        WebSocketMessageDto webSocketMessage = JSONUtil.toBean(text, WebSocketMessageDto.class);
        if (webSocketMessage.getType().equals("心跳檢測")){
            logger.info("{}發(fā)送心跳檢測",userId);
        }

        else if (webSocketMessage.getType().equals("群發(fā)")){

            ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);
            WebSocketMessageDto messageDto = JSONUtil.toBean(text, WebSocketMessageDto.class);

            WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();
            webSocketMessageDto.setType("群發(fā)");
            webSocketMessageDto.setText(messageDto.getText());
            webSocketMessageDto.setReceiver("all");
            webSocketMessageDto.setSender(String.valueOf(userId));
            webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));

            blockingQueue.add(webSocketMessageDto);
            channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonPrettyStr(webSocketMessageDto)));
        }
        else{
            channel.writeAndFlush("請發(fā)送正確的格式");
        }
    }

    // 建立連接后觸發(fā)(有客戶端建立連接請求)
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("建立連接");
        super.channelActive(ctx);
    }

    // 連接斷開后觸發(fā)(有客戶端關(guān)閉連接請求)
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        Attribute<Integer> attr = ctx.channel().attr(AttributeKey.valueOf(ctx.channel().id().toString()));
        Integer userId = attr.get();
        logger.info("用戶ID:{} 斷開連接",userId);

        ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);
        channelGroup.remove(ctx.channel());
        ChannelContext.removeChannel(userId);

        WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();
        webSocketMessageDto.setType("用戶變更");
        List<OnLineUserVo> onlineUser = userService.getOnlineUser();
        webSocketMessageDto.setText(JSONUtil.toJsonStr(onlineUser));
        webSocketMessageDto.setReceiver("all");
        webSocketMessageDto.setSender("0");
        webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));
        channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(webSocketMessageDto)));
        super.channelInactive(ctx);
    }

// 建立連接后觸發(fā)(客戶端完成連接)
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
            WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
            String uri = handshakeComplete.requestUri();
            logger.info("uri: {}",uri);
            String token = getToken(uri);
            if (token == null){
                logger.warn("Token校驗(yàn)失敗");
                ctx.close();
                throw new BaseException("Token校驗(yàn)失敗");
            }
            logger.info("token: {}",token);

            Integer userId = null;
            try{
                Claims claims = JwtUtil.extractClaims(token);
                userId = Integer.valueOf((String) claims.get("userId"));
            }catch (Exception e){
                logger.warn("Token校驗(yàn)失敗");
                ctx.close();
                throw new BaseException("Token校驗(yàn)失敗");
            }
            // 向channel中的附件中添加用戶ID
            channelContext.addContext(userId,ctx.channel());
            ChannelContext.setChannel(userId,ctx.channel());
            ChannelContext.setChannelGroup(null,ctx.channel());

            ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);

            WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();
            webSocketMessageDto.setType("用戶變更");
            List<OnLineUserVo> onlineUser = userService.getOnlineUser();
            webSocketMessageDto.setText(JSONUtil.toJsonStr(onlineUser));
            webSocketMessageDto.setReceiver("all");
            webSocketMessageDto.setSender("0");
            webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));
            channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(webSocketMessageDto)));

        }
        super.userEventTriggered(ctx, evt);
    }
    private String getToken(String uri){
        if (uri.isEmpty()){
            return null;
        }
        if(!uri.contains("token")){
            return null;
        }
        String[] split = uri.split("\\?");
        if (split.length!=2){
            return null;
        }
        String[] split1 = split[1].split("=");
        if (split1.length!=2){
            return null;
        }
        return split1[1];
    }
}

4. 工具類

主要用來保存用戶信息的

不要問我為什么又有static又有普通方法,問就是懶得改,這里我直接保存的同一個(gè)群組,如果需要多群組的話,就需要建立SQL數(shù)據(jù)了

package org.example.payroll_management.websocket;

@Component
public class ChannelContext {

    private static final Map<Integer, Channel> USER_CHANNEL_MAP = new ConcurrentHashMap<>();
    private static final Map<Integer, ChannelGroup> USER_CHANNELGROUP_MAP = new ConcurrentHashMap<>();
    private static final Integer GROUP_ID = 10086;

    private static final Logger logger = LoggerFactory.getLogger(ChannelContext.class);

    public void addContext(Integer userId,Channel channel){
        String channelId = channel.id().toString();
        AttributeKey attributeKey = null;
        if (AttributeKey.exists(channelId)){
            attributeKey = AttributeKey.valueOf(channelId);
        } else{
            attributeKey = AttributeKey.newInstance(channelId);
        }
        channel.attr(attributeKey).set(userId);
    }
    public static List<Integer> getAllUserId(){
        return new ArrayList<>(USER_CHANNEL_MAP.keySet());
    }
    public static void setChannel(Integer userId,Channel channel){
        USER_CHANNEL_MAP.put(userId,channel);
    }

    public static Channel getChannel(Integer userId){
        return USER_CHANNEL_MAP.get(userId);
    }
    public static void removeChannel(Integer userId){
        USER_CHANNEL_MAP.remove(userId);
    }

    public static void setChannelGroup(Integer groupId,Channel channel){
        if(groupId == null){
            groupId = GROUP_ID;
        }
        ChannelGroup channelGroup = USER_CHANNELGROUP_MAP.get(groupId);
        if (channelGroup == null){
            channelGroup =new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
            USER_CHANNELGROUP_MAP.put(GROUP_ID, channelGroup);
        }
        if (channel == null){
            return ;
        }
        channelGroup.add(channel);
        logger.info("向group中添加channel,ChannelGroup已有Channel數(shù)量:{}",channelGroup.size());

    }

    public static ChannelGroup getChannelGroup(Integer groupId){
        if (groupId == null){
            groupId = GROUP_ID;
        }
        return USER_CHANNELGROUP_MAP.get(groupId);
    }
    public static void removeChannelGroup(Integer groupId){
        if (groupId == null){
            groupId = GROUP_ID;
        }
         USER_CHANNELGROUP_MAP.remove(groupId);
    }
}

寫到這里,Netty服務(wù)就搭建完成了,后面就可以等著前端的請求建立了

前端

前端我使用的vue,因?yàn)槲蚁M?dāng)用戶登錄后自動(dòng)建立ws連接,所以我在登錄成功后添加上了ws建立請求,然后我發(fā)現(xiàn),如果用戶關(guān)閉網(wǎng)頁后重新打開,因?yàn)樘^了登錄界面,ws請求不會(huì)自動(dòng)建立,所以需要一套全局的ws請求

不過我前端不是很好(其實(shí)后端也一般),所以很多地方肯定有更優(yōu)的寫法

1. pinia

使用pinia保存ws請求,方便在其他組件中調(diào)用

定義WebSocket實(shí)例(ws)和一個(gè)請求建立判斷(wsConnect)

后面就可以通過ws接收服務(wù)的消息

import { defineStore } from 'pinia'

export const useWebSocketStore = defineStore('webSocket', {

    state() {
        return {
            ws: null,
            wsConnect: false,
        }
    },
    actions: {
        wsInit() {
            if (this.ws === null) {
                const token = localStorage.getItem("token")
                if (token === null)  return;
                this.ws = new WebSocket(`ws://localhost:8081/ws?token=${token}`)
                  
                this.ws.onopen = () => {
                    this.wsConnect = true;
                    console.log("ws協(xié)議建立成功")
                    // 發(fā)送心跳
                    const intervalId = setInterval(() => {
                        if (!this.wsConnect) {
                            clearInterval(intervalId)
                        }
                        const webSocketMessageDto = {
                            type: "心跳檢測"
                        }
                        this.sendMessage(JSON.stringify(webSocketMessageDto));
                    }, 1000 * 3 * 60);
                }
                this.ws.onclose = () => {
                    this.ws = null;
                    this.wsConnect = false;
                }
            }
        },
        sendMessage(message) {
            if (message == null || message == '') {
                return;
            }
            if (!this.wsConnect) {
                console.log("ws協(xié)議沒有建立")
                this.wsInit();
            }
            this.ws.send(message);
        },
        wsClose() {
            if (this.wsConnect) {
                this.ws.close();
                this.wsConnect = false;
            }
        }
    }
})

然后再app.vue中循環(huán)建立連接(建立請求重試)

 const wsConnect = function () {
        const token = localStorage.getItem("token")
        if (token === null) {
            return;
        }
        try {
            if (!webSocket.wsConnect) {
                console.log("嘗試建立ws請求")
                webSocket.wsInit();
            } else {
                return;
            }
        } catch {
            wsConnect();
        }
    }

2. 聊天組件

界面相信大伙都會(huì)畫,主要說一下我遇到的問題

第一個(gè) 上拉刷新,也就是加載歷史記錄的功能,我用的element-plus UI,也不知道是不是我的問題,UI里面的無限滾動(dòng)不是重復(fù)發(fā)送請求就是無限發(fā)送請求,而且好像沒有上拉加載的功能。于是我用了IntersectionObserver來解決,在頁面底部加上一個(gè)div,當(dāng)觀察到這個(gè)div時(shí),觸發(fā)請求

第二個(gè) 滾動(dòng)條到達(dá)頂部時(shí),請求數(shù)據(jù)并放置數(shù)據(jù),滾動(dòng)條會(huì)自動(dòng)滾動(dòng)到頂部,并且由于觀察的元素始終在頂端導(dǎo)致無限請求,這個(gè)其實(shí)也不是什么大問題,因?yàn)榱奶斓南⑹怯邢薜?,沒有數(shù)據(jù)之后我設(shè)置了停止觀察,主要是用戶體驗(yàn)不是很好。這是我是添加了display: flex; flex-direction: column-reverse;解決這個(gè)問題的(flex很神奇吧)。大致原理好像是垂直翻轉(zhuǎn)了(例如上面我將觀察元素放到div第一個(gè)子元素位置,添加flex后觀察元素會(huì)到最后一個(gè)子元素位置上),也就是說當(dāng)滾動(dòng)條在最底部時(shí),添加數(shù)據(jù)后,滾動(dòng)條會(huì)自動(dòng)滾動(dòng)到最底部,不過這樣體驗(yàn)感非常的不錯(cuò)

不要問我為什么數(shù)據(jù)要加 || 問就是數(shù)據(jù)懶得統(tǒng)一了

<style lang="scss" scoped>
    .chatBox {
        border-radius: 20px;
        box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;
        width: 1200px;
        height: 600px;
        background-color: white;
        display: flex;

        .chat {
            width: 1000px;
            height: inherit;

            .chatBackground {
                height: 500px;
                overflow: auto;
                display: flex;
                flex-direction: column-reverse;

                .loading {
                    text-align: center;
                    font-size: 12px;
                    margin-top: 20px;
                    color: gray;

                }

                .chatItem {
                    width: 100%;
                    padding-bottom: 20px;

                    .avatar {
                        margin-left: 20px;
                        display: flex;
                        align-items: center;

                        .username {
                            margin-left: 10px;
                            color: rgb(153, 153, 153);
                            font-size: 13px;
                        }
                    }

                    .chatItemMessage {
                        margin-left: 60px;
                        padding: 10px;
                        font-size: 14px;
                        width: 200px;
                        word-break: break-all;
                        max-width: 400px;
                        line-height: 25px;
                        width: fit-content;
                        border-radius: 10px;
                        height: auto;
                        /* background-color: skyblue; */
                        box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;
                    }

                    .sendDate {
                        font-size: 12px;
                        margin-top: 10px;
                        margin-left: 60px;
                        color: rgb(187, 187, 187);
                    }
                }
            }

            .chatBottom {
                height: 100px;
                background-color: #F3F3F3;
                border-radius: 20px;
                display: flex;
                box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;

                .messageInput {
                    border-radius: 20px;
                    width: 400px;
                    height: 40px;
                }

            }

        }

        .userList {
            width: 200px;
            height: inherit;
            border-radius: 20px;
            box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;

            .user {
                width: inherit;
                height: 50px;
                line-height: 50px;
                text-indent: 2em;
                border-radius: 20px;
                transition: all 0.5s ease;
            }
        }
    }

    .user:hover {
        box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;
        transform: translateX(-5px) translateY(-5px);
    }
</style>

<template>
    {{hasMessage}}
    <div class="chatBox">
        <div class="chat">
            <div class="chatBackground" ref="chatBackgroundRef">

                <div class="chatItem" v-for="i in messageList">
                    <div class="avatar">
                        <el-avatar :size="40" :src="imageUrl" />
                        <div class="username">{{i.username || i.userId}}</div>
                    </div>
                    <div class="chatItemMessage">
                        {{i.text || i.content}}
                    </div>
                    <div class="sendDate">
                        {{i.date || i.sendDate}}
                    </div>

                </div>
                <div class="loading" ref="loading">
                    顯示更多內(nèi)容
                </div>
            </div>
            <div class="chatBottom">
                <el-input class="messageInput" v-model="message" placeholder="消息內(nèi)容"></el-input>
                <el-button @click="sendMessage">發(fā)送消息</el-button>
            </div>
        </div>
        <!-- 做成無限滾動(dòng) -->
        <div class="userList">
            <div v-for="user in userList">
                <div class="user">
                    {{user.userName}}
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
    import { ref, onMounted, nextTick } from 'vue'
    import request from '@/utils/request.js'
    import { useWebSocketStore } from '@/stores/useWebSocketStore'
    import imageUrl from '@/assets/默認(rèn)頭像.jpg'


    const webSocketStore = useWebSocketStore();
    const chatBackgroundRef = ref(null)

    const userList = ref([])
    const message = ref('')
    const messageList = ref([
    ])
    const loading = ref(null)
    const page = ref(1);
    const size = 10;
    const hasMessage = ref(true);

    const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(async entry => {
            if (entry.isIntersecting) {
                observer.unobserve(entry.target)
                await pageQueryMessage();

            }
        })
    })

    onMounted(() => {
        observer.observe(loading.value)
        getOnlineUserList();
        if (!webSocketStore.wsConnect) {
            webSocketStore.wsInit();
        }
        const ws = webSocketStore.ws;
        ws.onmessage = async (e) => {
            // console.log(e);
            const webSocketMessage = JSON.parse(e.data);
            const messageObj = {
                username: webSocketMessage.sender,
                text: webSocketMessage.text,
                date: webSocketMessage.sendDate,
                type: webSocketMessage.type
            }
            console.log("###")
            // console.log(JSON.parse(messageObj.text))
            if (messageObj.type === "群發(fā)") {
                messageList.value.unshift(messageObj)
            } else if (messageObj.type === "用戶變更") {
                userList.value = JSON.parse(messageObj.text)
            }
            await nextTick();
            // 當(dāng)發(fā)送新消息時(shí),自動(dòng)滾動(dòng)到頁面最底部,可以替換成消息提示的樣式
            // chatBackgroundRef.value.scrollTop = chatBackgroundRef.value.scrollHeight;
            console.log(webSocketMessage)
        }
    })
    const pageQueryMessage = function () {
        request({
            url: '/api/message/pageQueryMessage',
            method: 'post',
            data: {
                page: page.value,
                size: size
            }
        }).then((res) => {
            console.log(res)
            if (res.data.data.length === 0) {
                hasMessage.value = false;
            }
            else {
                observer.observe(loading.value)
                page.value = page.value + 1;
                messageList.value.push(...res.data.data)
            }
        })
    }
    function getOnlineUserList() {
        request({
            url: '/api/user/getOnlineUser',
            method: 'get'
        }).then((res) => {
            console.log(res)
            userList.value = res.data.data;
        })
    }

    const sendMessage = function () {
        if (!webSocketStore.wsConnect) {
            webSocketStore.wsInit();
        }
        const webSocketMessageDto = {
            type: "群發(fā)",
            text: message.value
        }

        webSocketStore.sendMessage(JSON.stringify(webSocketMessageDto));
    }

</script>

這樣就實(shí)現(xiàn)了一個(gè)簡易的聊天數(shù)據(jù)持久化,支持在線聊天的界面,總的來說WebSocket用起來還是十分方便的

到此這篇關(guān)于SpringBoot+Netty+Vue+WebSocket實(shí)現(xiàn)在線聊天的文章就介紹到這了,更多相關(guān)SpringBoot Netty Vue WebSocket在線聊天內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java 中JFinal getModel方法和數(shù)據(jù)庫使用出現(xiàn)問題解決辦法

    java 中JFinal getModel方法和數(shù)據(jù)庫使用出現(xiàn)問題解決辦法

    這篇文章主要介紹了java 中JFinal getModel方法和數(shù)據(jù)庫使用出現(xiàn)問題解決辦法的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • SpringBoot如何使用注解進(jìn)行XSS防御

    SpringBoot如何使用注解進(jìn)行XSS防御

    在SpringBoot中,可以通過自定義@XSS注解和實(shí)現(xiàn)XSSValidator類來防御XSS攻擊,此方法適用于GET和POST請求,通過在方法參數(shù)或?qū)嶓w類屬性上添加@XSS注解,并結(jié)合@Valid或@Validated注解使用,有效攔截潛在的XSS腳本,保障應(yīng)用安全
    2024-11-11
  • Java實(shí)現(xiàn)用戶不可重復(fù)登錄功能

    Java實(shí)現(xiàn)用戶不可重復(fù)登錄功能

    這篇文章主要介紹了Java實(shí)現(xiàn)用戶不可重復(fù)登錄功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下
    2016-12-12
  • Java使用自定義注解實(shí)現(xiàn)函數(shù)測試功能示例

    Java使用自定義注解實(shí)現(xiàn)函數(shù)測試功能示例

    這篇文章主要介紹了Java使用自定義注解實(shí)現(xiàn)函數(shù)測試功能,結(jié)合實(shí)例形式分析了java自定義注解在函數(shù)測試過程中相關(guān)功能、原理與使用技巧,需要的朋友可以參考下
    2019-10-10
  • Spring?AOP核心功能示例代碼詳解

    Spring?AOP核心功能示例代碼詳解

    AOP面向切面編程,它是一種思想,它是對(duì)某一類事情的集中處理,而AOP是一種思想,而Spring?AOP是一個(gè)框架,提供了一種對(duì)AOP思想的實(shí)現(xiàn),它們的關(guān)系和loC與DI類似,這篇文章主要介紹了Spring?AOP統(tǒng)一功能處理示例代碼,需要的朋友可以參考下
    2023-02-02
  • Spring Cloud OpenFeign REST服務(wù)客戶端原理及用法解析

    Spring Cloud OpenFeign REST服務(wù)客戶端原理及用法解析

    這篇文章主要介紹了Spring Cloud OpenFeign REST服務(wù)客戶端原理及用法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-10-10
  • hadoop?全面解讀自定義分區(qū)

    hadoop?全面解讀自定義分區(qū)

    Hadoop是一個(gè)由Apache基金會(huì)所開發(fā)的分布式系統(tǒng)基礎(chǔ)架構(gòu)。用戶可以在不了解分布式底層細(xì)節(jié)的情況下,開發(fā)分布式程序。充分利用集群的威力進(jìn)行高速運(yùn)算和存儲(chǔ)
    2022-02-02
  • Spring MVC多種情況下進(jìn)行文件上傳的實(shí)例

    Spring MVC多種情況下進(jìn)行文件上傳的實(shí)例

    上傳是Web工程中很常見的功能,SpringMVC框架簡化了文件上傳的代碼,本文給大家總結(jié)了Spring MVC多種情況下進(jìn)行文件上傳的實(shí)例,并通過代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2024-02-02
  • Java中對(duì)null進(jìn)行強(qiáng)制類型轉(zhuǎn)換的方法

    Java中對(duì)null進(jìn)行強(qiáng)制類型轉(zhuǎn)換的方法

    小編對(duì)null進(jìn)行強(qiáng)轉(zhuǎn)會(huì)不會(huì)拋錯(cuò),非常的好奇,下面小編通過實(shí)例代碼給大家介紹Java中對(duì)null進(jìn)行強(qiáng)制類型轉(zhuǎn)換的方法,感興趣的朋友參考下吧
    2018-09-09
  • 如何將JSON字符串?dāng)?shù)組轉(zhuǎn)對(duì)象集合

    如何將JSON字符串?dāng)?shù)組轉(zhuǎn)對(duì)象集合

    這篇文章主要介紹了如何將JSON字符串?dāng)?shù)組轉(zhuǎn)對(duì)象集合,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-06-06

最新評(píng)論