SpringBoot+WebSocket:在线聊天室从0到1

29 阅读9分钟

《SpringBoot+WebSocket:在线聊天室从0到1》

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

我是小坏,今天咱们聊点有意思的。你做过聊天功能吗?是不是还在用轮询,隔几秒就问一次"有新消息吗"?太费资源了!今天教你用WebSocket,实现真正的实时通信。

一、HTTP轮询的痛

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

看看传统的做法

// 前端:每5秒问一次
setInterval(() => {
    fetch('/api/messages')
        .then(res => res.json())
        .then(messages => {
            // 处理消息
        })
}, 5000);

问题

  • 浪费带宽:大部分请求都是空跑
  • 延迟高:最坏情况要等5秒
  • 服务器压力大:1万用户就是1万次/5秒的请求

WebSocket的优势

  • 一次连接,双向通信
  • 真正的实时:消息立即到达
  • 节省资源:没有重复的HTTP头部

二、5分钟搭建聊天服务

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

2.1 加个依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.2 写个配置

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Autowired
    private ChatHandler chatHandler;
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/chat")
                .setAllowedOrigins("*");  // 允许跨域
    }
}

2.3 写个处理器

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Component
@Slf4j
public class ChatHandler extends TextWebSocketHandler {
    
    // 保存所有连接
    private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String userId = getUserId(session);
        sessions.put(userId, session);
        log.info("用户 {} 连接成功", userId);
        
        // 通知所有人:用户上线
        broadcast(userId + " 上线了");
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        String msg = message.getPayload();
        String userId = getUserId(session);
        
        log.info("收到消息:{},来自:{}", msg, userId);
        
        // 处理消息
        handleMessage(userId, msg);
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String userId = getUserId(session);
        sessions.remove(userId);
        log.info("用户 {} 断开连接", userId);
        
        // 通知所有人:用户下线
        broadcast(userId + " 下线了");
    }
}

2.4 前端连接

<!DOCTYPE html>
<html>
<body>
    <input id="message" placeholder="输入消息">
    <button onclick="send()">发送</button>
    <div id="chat"></div>
    
    <script>
        const ws = new WebSocket('ws://localhost:8080/chat?userId=123');
        
        ws.onopen = () => {
            console.log('连接成功');
        };
        
        ws.onmessage = (event) => {
            const msg = event.data;
            document.getElementById('chat').innerHTML += msg + '<br>';
        };
        
        function send() {
            const msg = document.getElementById('message').value;
            ws.send(msg);
        }
    </script>
</body>
</html>

三、核心功能实现

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

3.1 单聊(一对一)

@Service
public class ChatService {
    
    // 发送私聊消息
    public void sendPrivate(String fromUserId, String toUserId, String message) {
        WebSocketSession toSession = sessions.get(toUserId);
        
        if (toSession != null && toSession.isOpen()) {
            // 对方在线,直接发送
            toSession.sendMessage(new TextMessage(formatPrivateMsg(fromUserId, message)));
        } else {
            // 对方不在线,存到数据库
            saveOfflineMessage(fromUserId, toUserId, message);
        }
    }
    
    // 格式:私聊消息
    private String formatPrivateMsg(String fromUserId, String message) {
        return JSON.toJSONString(Map.of(
            "type", "private",
            "from", fromUserId,
            "time", new Date(),
            "content", message
        ));
    }
}

3.2 群聊(聊天室)

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Service
public class GroupChatService {
    
    // 群成员映射:群ID -> 用户ID列表
    private final Map<String, Set<String>> groups = new ConcurrentHashMap<>();
    
    // 加入群聊
    public void joinGroup(String groupId, String userId) {
        groups.computeIfAbsent(groupId, k -> new HashSet<>())
              .add(userId);
        
        // 通知群里其他人
        sendToGroup(groupId, userId + " 加入了群聊");
    }
    
    // 发送群消息
    public void sendToGroup(String groupId, String fromUserId, String message) {
        Set<String> members = groups.get(groupId);
        if (members == null) return;
        
        String formattedMsg = JSON.toJSONString(Map.of(
            "type", "group",
            "groupId", groupId,
            "from", fromUserId,
            "time", new Date(),
            "content", message
        ));
        
        // 发给群里所有人(除了自己)
        for (String memberId : members) {
            if (!memberId.equals(fromUserId)) {
                WebSocketSession session = sessions.get(memberId);
                if (session != null && session.isOpen()) {
                    session.sendMessage(new TextMessage(formattedMsg));
                }
            }
        }
    }
}

3.3 广播(系统消息)

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Service
public class BroadcastService {
    
    // 广播给所有人
    public void broadcast(String message) {
        String formattedMsg = JSON.toJSONString(Map.of(
            "type", "broadcast",
            "time", new Date(),
            "content", message
        ));
        
        for (WebSocketSession session : sessions.values()) {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(formattedMsg));
            }
        }
    }
    
    // 广播给特定角色
    public void broadcastToRole(String role, String message) {
        // 先查询有该角色的用户
        List<String> userIds = userService.findByRole(role);
        
        for (String userId : userIds) {
            WebSocketSession session = sessions.get(userId);
            if (session != null && session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            }
        }
    }
}

四、进阶功能

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

4.1 心跳检测(防假死)

@Component
@Slf4j
public class HeartbeatHandler extends TextWebSocketHandler {
    
    // 心跳超时时间:30秒
    private static final long HEARTBEAT_TIMEOUT = 30000;
    
    // 用户最后心跳时间
    private final Map<String, Long> lastHeartbeat = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        // 定时检查心跳
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(this::checkHeartbeat, 30, 30, TimeUnit.SECONDS);
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        String msg = message.getPayload();
        
        if ("ping".equals(msg)) {
            // 收到心跳
            String userId = getUserId(session);
            lastHeartbeat.put(userId, System.currentTimeMillis());
            
            // 回复pong
            session.sendMessage(new TextMessage("pong"));
        } else {
            // 其他消息
            super.handleTextMessage(session, message);
        }
    }
    
    // 检查心跳
    private void checkHeartbeat() {
        long now = System.currentTimeMillis();
        
        for (Map.Entry<String, Long> entry : lastHeartbeat.entrySet()) {
            if (now - entry.getValue() > HEARTBEAT_TIMEOUT) {
                String userId = entry.getKey();
                WebSocketSession session = sessions.get(userId);
                
                if (session != null) {
                    try {
                        session.close();
                        log.warn("用户 {} 心跳超时,断开连接", userId);
                    } catch (IOException e) {
                        log.error("断开连接失败", e);
                    }
                }
            }
        }
    }
}

4.2 断线重连

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

// 前端实现
let ws = null;
let reconnectTimer = null;

function connect() {
    ws = new WebSocket('ws://localhost:8080/chat');
    
    ws.onopen = () => {
        console.log('连接成功');
        clearTimeout(reconnectTimer);
    };
    
    ws.onclose = () => {
        console.log('连接断开,5秒后重连');
        reconnectTimer = setTimeout(connect, 5000);
    };
    
    ws.onerror = (error) => {
        console.error('连接错误', error);
        ws.close();
    };
}

// 开始连接
connect();

4.3 消息历史

@Service
public class MessageHistoryService {
    
    // 保存聊天记录
    public void saveMessage(String fromUserId, String toUserId, 
                           String type, String content) {
        ChatMessage message = new ChatMessage();
        message.setFromUserId(fromUserId);
        message.setToUserId(toUserId);
        message.setType(type);  // private, group, broadcast
        message.setContent(content);
        message.setCreateTime(new Date());
        
        chatMessageRepository.save(message);
    }
    
    // 获取历史消息
    public List<ChatMessage> getHistory(String userId, String targetId, 
                                        String type, int limit) {
        Pageable pageable = PageRequest.of(0, limit, 
            Sort.by(Sort.Direction.DESC, "createTime"));
        
        if ("private".equals(type)) {
            return chatMessageRepository.findPrivateHistory(
                userId, targetId, pageable);
        } else if ("group".equals(type)) {
            return chatMessageRepository.findGroupHistory(
                targetId, pageable);
        }
        
        return Collections.emptyList();
    }
}

五、性能优化

5.1 连接数限制

@Component
public class ConnectionLimitHandler extends TextWebSocketHandler {
    
    // 最大连接数
    private static final int MAX_CONNECTIONS = 10000;
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        if (sessions.size() >= MAX_CONNECTIONS) {
            session.close(CloseStatus.POLICY_VIOLATION);
            return;
        }
        
        super.afterConnectionEstablished(session);
    }
}

5.2 消息压缩

// 配置消息大小限制
@Bean
public WebSocketServletWebHandlerCustomizer webSocketHandlerCustomizer() {
    return handler -> {
        handler.setMessageSizeLimit(128 * 1024);  // 128KB
        handler.setSendBufferSizeLimit(512 * 1024);  // 512KB
    };
}

// 发送时压缩
public void sendCompressedMessage(WebSocketSession session, String message) {
    if (message.length() > 1024) {  // 大于1KB才压缩
        byte[] compressed = compress(message);
        session.sendMessage(new BinaryMessage(compressed));
    } else {
        session.sendMessage(new TextMessage(message));
    }
}

5.3 集群部署

@Configuration
public class RedisWebSocketConfig {
    
    @Bean
    public SimpMessageSendingOperations messagingTemplate() {
        // 使用Redis广播消息
        return new SimpMessagingTemplate(brokerChannel);
    }
    
    @Bean
    public MessageChannel brokerChannel() {
        return new MessageChannel() {
            // 集成Redis Pub/Sub
        };
    }
}

六、实战:在线聊天室完整实现

6.1 消息格式定义

@Data
public class ChatMessage {
    private String type;      // 类型:connect, chat, image, file, system
    private String from;      // 发送者
    private String to;        // 接收者(用户ID或群ID)
    private String content;   // 内容
    private Long time;        // 时间戳
    private Map<String, Object> extra;  // 扩展字段
}

6.2 完整处理器

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Component
@Slf4j
public class ChatServerHandler extends TextWebSocketHandler {
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String userId = getUserId(session);
        
        // 保存连接
        sessions.put(userId, session);
        
        // 发送连接成功消息
        sendSystemMessage(session, "连接成功");
        
        // 发送未读消息
        sendUnreadMessages(userId);
        
        // 通知好友上线
        notifyFriendsOnline(userId);
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, 
                                     TextMessage textMessage) {
        try {
            ChatMessage message = JSON.parseObject(
                textMessage.getPayload(), 
                ChatMessage.class
            );
            
            // 根据消息类型处理
            switch (message.getType()) {
                case "chat":
                    handleChatMessage(session, message);
                    break;
                case "image":
                    handleImageMessage(session, message);
                    break;
                case "file":
                    handleFileMessage(session, message);
                    break;
                case "heartbeat":
                    handleHeartbeat(session, message);
                    break;
                default:
                    log.warn("未知消息类型: {}", message.getType());
            }
        } catch (Exception e) {
            log.error("处理消息失败", e);
            sendErrorMessage(session, "消息格式错误");
        }
    }
    
    // 处理聊天消息
    private void handleChatMessage(WebSocketSession session, 
                                   ChatMessage message) {
        String fromUserId = getUserId(session);
        
        if (message.getTo().startsWith("group_")) {
            // 群消息
            groupChatService.sendToGroup(
                message.getTo(),
                fromUserId,
                message.getContent()
            );
        } else {
            // 私聊消息
            chatService.sendPrivate(
                fromUserId,
                message.getTo(),
                message.getContent()
            );
        }
        
        // 保存消息记录
        messageHistoryService.saveMessage(
            fromUserId,
            message.getTo(),
            message.getType(),
            message.getContent()
        );
    }
}

6.3 前端完整实现

<!DOCTYPE html>
<html>
<head>
    <title>在线聊天室</title>
    <style>
        .chat-container { width: 600px; margin: 0 auto; }
        .messages { height: 400px; border: 1px solid #ccc; overflow-y: scroll; }
        .input-area { margin-top: 10px; }
        .online-users { width: 200px; float: right; }
    </style>
</head>
<body>
    <div class="chat-container">
        <h2>在线聊天室</h2>
        
        <div class="online-users">
            <h3>在线用户</h3>
            <ul id="userList"></ul>
        </div>
        
        <div class="messages" id="messages"></div>
        
        <div class="input-area">
            <input type="text" id="messageInput" placeholder="输入消息...">
            <button onclick="sendMessage()">发送</button>
            <button onclick="sendImage()">发送图片</button>
            <select id="receiver">
                <option value="all">所有人</option>
            </select>
        </div>
    </div>
    
    <script>
        // WebSocket连接
        let ws = null;
        let userId = null;
        
        // 初始化
        function init() {
            userId = prompt("请输入你的用户名");
            if (!userId) return;
            
            connect();
            loadHistory();
        }
        
        // 连接WebSocket
        function connect() {
            ws = new WebSocket(`ws://localhost:8080/chat?userId=${userId}`);
            
            ws.onopen = onConnected;
            ws.onmessage = onMessage;
            ws.onclose = onDisconnected;
            ws.onerror = onError;
        }
        
        // 发送消息
        function sendMessage() {
            const input = document.getElementById('messageInput');
            const receiver = document.getElementById('receiver').value;
            
            const message = {
                type: 'chat',
                from: userId,
                to: receiver,
                content: input.value,
                time: Date.now()
            };
            
            ws.send(JSON.stringify(message));
            input.value = '';
        }
        
        // 处理收到的消息
        function onMessage(event) {
            const message = JSON.parse(event.data);
            
            switch (message.type) {
                case 'chat':
                    displayMessage(message);
                    break;
                case 'system':
                    displaySystemMessage(message);
                    break;
                case 'userList':
                    updateUserList(message.users);
                    break;
            }
        }
        
        // 显示消息
        function displayMessage(message) {
            const messagesDiv = document.getElementById('messages');
            const msgHtml = `
                <div class="message">
                    <strong>${message.from}</strong>
                    <span>${new Date(message.time).toLocaleTimeString()}</span>
                    <p>${message.content}</p>
                </div>
            `;
            messagesDiv.innerHTML += msgHtml;
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
        
        // 初始化
        window.onload = init;
    </script>
</body>
</html>

七、避坑指南

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

坑1:Nginx代理

# ❌ 错误:普通HTTP代理
location /chat {
    proxy_pass http://backend;
}

# ✅ 正确:WebSocket代理
location /chat {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

坑2:连接数限制

// 解决:调整服务器配置
@Bean
public WebSocketServletServerContainerFactory containerFactory() {
    TomcatServletWebSocketContainerFactory factory = 
        new TomcatServletWebSocketContainerFactory();
    factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
    factory.setMaxSessionIdleTimeout(15 * 60 * 1000L);  // 15分钟
    factory.setMaxBinaryMessageBufferSize(1024 * 1024);  // 1MB
    factory.setMaxTextMessageBufferSize(512 * 1024);     // 512KB
    return factory;
}

坑3:消息顺序

// 解决:给消息加序列号
let seq = 0;

function sendMessage(content) {
    const message = {
        seq: seq++,
        content: content,
        timestamp: Date.now()
    };
    
    ws.send(JSON.stringify(message));
}

八、今日要点总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

  1. WebSocket vs HTTP轮询:真实时 vs 假实时
  2. 5步搭建:加依赖 → 写配置 → 处理器 → 前端连接 → 完成
  3. 三种聊天模式:单聊、群聊、广播
  4. 进阶功能:心跳检测、断线重连、消息历史
  5. 性能优化:连接限制、消息压缩、集群部署
  6. 生产注意:Nginx配置、连接数限制、消息顺序

九、思考题

场景:你要做一个万人直播聊天室

  1. 如何管理上万连接?
  2. 如何防止消息风暴?
  3. 如何实现禁言、踢人功能?
  4. 如何保证消息不丢失?

评论区聊聊你的方案,明天咱们讲任务调度。


明天预告:《SpringBoot定时任务:从简单到集群》

今日福利:关注后回复"WebSocket",获取完整聊天室源码。


公众号运营小贴士:

💡 互动

  1. 你做过实时通信功能吗?用的什么方案?
  2. 投票:你觉得WebSocket最难的部分是什么?
  3. 留言提问,明天文章解答

🎁 福利

  1. 留言区抽3人送《WebSocket实战》
  2. 转发截图送企业级聊天室源码