springboot实现websocket在线聊天室

学习一下websocket通信

效果图

前端代码,在当前目录src\main\resources\static 创建chat.html

复制代码
<!DOCTYPE` `html>`
`<html>`
`<head>`
    `<meta` `charset="UTF-8">`
    `<title>WebSocket聊天室</title>`
    `<style>`
        `body` `{` `font-family:` `Arial,` `sans-serif;` `max-width:` `800px;` `margin:` `0` `auto;` `padding:` `20px;` `}`
`        #messages` `{` `border:` `1px` `solid #ccc;` `height:` `400px;` `overflow-y:` `scroll;` `padding:` `10px;` `margin-bottom:` `10px;` `background: #f9f9f9;` `}`
        `.system` `{` `color:` `gray;` `font-style:` `italic;` `}`
        `.private` `{` `color:` `purple;` `font-weight:` `bold;` `}`
        `input` `{` `padding:` `8px;` `width:` `70%;` `margin-right:` `5px;` `}`
        `button` `{` `padding:` `8px` `15px;` `background: #007bff;` `color:` `white;` `border:` `none;` `cursor:` `pointer;` `}`
        `button:hover` `{` `background: #0056b3;` `}`
`        #status` `{` `color:` `green;` `margin-bottom:` `10px;` `}`
    `</style>`
`</head>`
`<body>`
`<h2>WebSocket在线聊天室</h2>`
`<div` `id="status">🟢 正在连接...</div>`
`<div` `id="messages"></div>`

`<div>`
    `<input` `type="text"` `id="messageInput"` `placeholder="输入消息...(私聊格式:@用户名 消息内容)"` `/>`
    `<button` `onclick="sendMessage()">发送</button>`
    `<button` `onclick="closeConnection()">断开连接</button>`
`</div>`

`<script>`
    `// 创建WebSocket连接`
    `const` `socket` `=` `new` `WebSocket('ws://localhost:8080/chat');`

    `// 获取DOM元素`
    `const` `messagesDiv` `=` `document.getElementById('messages');`
    `const` `statusDiv` `=` `document.getElementById('status');`
    `const` `messageInput` `=` `document.getElementById('messageInput');`

    `// 连接打开`
    `socket.onopen` `=` `function()` `{`
        `statusDiv.innerHTML` `=` `'🟢 已连接';`
        `appendMessage('【系统】连接成功,可以开始聊天啦!',` `'system');`
    `};`

    `// 收到消息`
    `socket.onmessage` `=` `function(event)` `{`
        `const` `message` `=` `event.data;`

        `// 根据消息内容判断类型(简单判断)`
        `if` `(message.includes('【系统】'))` `{`
            `appendMessage(message,` `'system');`
        `}` `else` `if` `(message.includes('【私聊】'))` `{`
            `appendMessage(message,` `'private');`
        `}` `else` `{`
            `appendMessage(message);`
        `}`
    `};`

    `// 连接关闭`
    `socket.onclose` `=` `function()` `{`
        `statusDiv.innerHTML` `=` `'🔴 已断开连接';`
        `appendMessage('【系统】连接已断开',` `'system');`
    `};`

    `// 连接错误`
    `socket.onerror` `=` `function(error)` `{`
        `console.error('WebSocket错误:',` `error);`
        `appendMessage('【系统】连接发生错误',` `'system');`
    `};`

    `// 发送消息`
    `function` `sendMessage()` `{`
        `const` `message` `=` `messageInput.value.trim();`
        `if` `(message` `===` `'')` `{`
            `alert('请输入消息');`
            `return;`
        `}`

        `if` `(socket.readyState` `===` `WebSocket.OPEN)` `{`
            `socket.send(message);`
            `messageInput.value` `=` `'';`
        `}` `else` `{`
            `alert('连接未建立或已断开');`
        `}`
    `}`

    `// 关闭连接`
    `function` `closeConnection()` `{`
        `socket.close();`
    `}`

    `// 添加消息到显示区域`
    `function` `appendMessage(message,` `type` `=` `'normal')` `{`
        `const` `msgElement` `=` `document.createElement('div');`
        `msgElement.textContent` `=` `message;`

        `if` `(type` `===` `'system')` `{`
            `msgElement.style.color` `=` `'gray';`
            `msgElement.style.fontStyle` `=` `'italic';`
        `}` `else` `if` `(type` `===` `'private')` `{`
            `msgElement.style.color` `=` `'purple';`
            `msgElement.style.fontWeight` `=` `'bold';`
        `}`

        `messagesDiv.appendChild(msgElement);`
        `// 自动滚动到底部`
        `messagesDiv.scrollTop` `=` `messagesDiv.scrollHeight;`
    `}`

    `// 回车发送`
    `messageInput.addEventListener('keypress',` `function(e)` `{`
        `if` `(e.key` `===` `'Enter')` `{`
            `sendMessage();`
        `}`
    `});`
`</script>`
`</body>`
`</html>`
`

后端使用springboot框架

首先添加maven依赖

复制代码
 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version> <!-- 或者用 2.7.18,看你需要哪个版本 -->
        <relativePath/> <!-- 从仓库查找 -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

添加完后,核心配置类

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

为什么需要这个配置,springboot默认不会扫描@ServerEndpoint注解,需要ServerEndpointExporter来注册他们

核心:websocket处理器

java 复制代码
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

@Component
@ServerEndpoint("/chat")
public class ChatWebSocketServer {

    private static final CopyOnWriteArrayList<ChatWebSocketServer> connections =
            new CopyOnWriteArrayList<>();

    private static final ConcurrentHashMap<String,ChatWebSocketServer> userMap  =
            new ConcurrentHashMap<>();

    private Session session;

    private String username;

    /**
     * 连接建立成功调用
     *
     */
    @OnOpen
    public void onOpen(Session session){
        this.session = session;
        this.username = "User-"+session.getId();
        connections.add(this);
        userMap.put(username,this);

        // 广播 某某加入了聊天室
        broadcaseMessage("【系统】"+username+" 加入了聊天室,当前在线人事"+connections.size());
        System.out.println("新连接:"+username+",当前在线:"+connections.size());
    }

    /**
     * 收到客户端的消息
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message,Session session){

        System.out.println("来自 "+username+" 的消息:"+message);

        // 处理私聊格式 :@用名 消息内容
        if(message.startsWith("@")){
            int firstSpace = message.indexOf(" ");
            if(firstSpace > 1){
                String targetUser = message.substring(1,firstSpace);
                String privateMsg = message.substring(firstSpace+1);

                sendPrivateMsg(targetUser,"【私聊】"+username+" 对你说:"+privateMsg);
                sendMessage("【私聊】你对 "+targetUser+" 说:"+privateMsg);
                return ;

            }
        }

        // 默认群发
        broadcaseMessage(username+ " : "+message);
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose(){
        connections.remove(this);
        if(username!=null){
            userMap.remove(username);
            broadcaseMessage("【系统】"+username+" 离开了聊天室,剩余人数"+connections.size());

        }else{
            // 用户名为空的情况下只广播有人离开了
            broadcaseMessage("【系统】有用户离开了聊天室,剩余人数"+connections.size());
        }
        System.out.println(username!=null?username:"未知用户" + " 断开了连接,当前在线:"+connections.size());


    }

    /**
     * 发生错误时候
     */
    @OnError
    public void onError(Throwable error){
        System.out.println("websocket 发生错误"+error.getMessage());
    }

    /**
     * 发送消息给自己
     */
    private void sendMessage(String message){
        try{
            if(session != null && session.isOpen()){
                session.getBasicRemote().sendText(message);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 发送私聊消息给指定用户
     */
    private void sendPrivateMsg(String targetUsername,String msg){
        ChatWebSocketServer target = userMap.get(targetUsername);
        if(target != null){
            target.sendMessage(msg);
        }else{
            sendMessage("【系统】用户"+targetUsername+" 不在线");
        }
    }

    /**
     * 广播
     */
    private static void broadcaseMessage(String message){
        for(ChatWebSocketServer client:connections){
            try{
                if(client.session!=null && client.session.isOpen()){
                    client.session.getBasicRemote().sendText(message);
                }
            }catch (IOException e){
                e.printStackTrace();
                // 发送失败就移除连接
                connections.remove(client);
                userMap.remove(client.username);
            }
        }
    }
}

CopyOnWriteArrayList 是读写安全的list,广播常用

ConcurrentHashMap 按照用户名查找会话,适合私聊场景

@ServerEndPoint 定义webSocket服务端点

@OnOpen,@OnClose,@OnMessage,@OnError生命周期事件

启动springboot项目,输入http://localhost:8080/chat.html

可以打开多个浏览器窗口模拟多个用户

普通消息是广播的形式,@用户名是使用私聊的方式

相关推荐
xuansec2 小时前
【JavaEE安全】Spring Boot 安全实战:Actuator 监控泄漏与 Swagger 接口利用
spring boot·安全·java-ee
onebyte8bits2 小时前
NestJS 系列教程(十五):缓存体系设计 —— Redis、接口缓存与缓存三大问题解决方案
数据库·redis·后端·缓存·nestjs
xuansec2 小时前
【JavaEE安全】Spring Boot 安全实战:从路由响应到 MyBatis 注入与 Thymeleaf SSTI
spring boot·安全·java-ee
于眠牧北2 小时前
分布式环境在@Transation注解下锁释放问题
spring boot·redis·分布式
MX_93592 小时前
Spring的xml方式声明式事务控制
xml·java·后端·spring
程序员爱钓鱼3 小时前
Go文件路径处理完全指南:path/filepath包详解与实战
后端·面试·go
爱笑的源码基地3 小时前
基于云计算的基层医疗信息系统,springMVC框架开发的云HIS系统源码
spring boot·后端·源码·二次开发·his·源代码·医院管理信息系统
乂爻yiyao4 小时前
Spring Boot Fat JAR 容器化指南
spring boot·后端·jar