WebSocket:让HTTP的“尬聊”变成真正的“畅聊”

121 阅读7分钟

大家好,我是小悟。

一、WebSocket是什么?—— HTTP的“社恐”表弟变身“社交牛逼症”

想象一下HTTP协议是个有点“社恐”的程序员:

  • 每次聊天都要先说“你好”,对方回“你好”,然后才能说正事
  • 说完一句话就必须闭嘴,等对方回应才能说下一句
  • 想实时聊天?得不停地问“你有新消息吗?”“现在呢?”“现在呢?”

而WebSocket就像HTTP喝了十杯咖啡的表弟:

  • 一次握手,终身连接(直到你主动分手)
  • 双向通话,随时插话
  • 真正的“你一句我一句”,不再是你问一句我答一句的“审讯式聊天”
// HTTP vs WebSocket 的日常对话对比

// HTTP的尬聊场景:
你:喂,在吗?(请求)
服务器:在的(响应)
你:吃了吗?(请求)
服务器:吃了(响应)
你:吃的啥?(请求)
服务器:...你烦不烦(响应)

// WebSocket的畅聊场景:
你:<连接建立>
你:吃了吗?
服务器:吃了,吃的炸鸡
服务器:你要不要也来点?
你:要要要!加杯可乐!
// ... 自由流畅的对话继续

二、SpringBoot集成WebSocket详细步骤

第1步:引入依赖——给项目“灌咖啡”

<!-- pom.xml -->
<dependencies>
    <!-- SpringBoot的WebSocket“咖啡包” -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    <!-- 前端用Stomp的“吸管”喝咖啡 -->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>stomp-websocket</artifactId>
        <version>2.3.4</version>
    </dependency>
    
    <!-- 前端用SockJS的“备用吸管”(万一主吸管坏了) -->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>sockjs-client</artifactId>
        <version>1.5.1</version>
    </dependency>
</dependencies>

第2步:配置类——搭建聊天室的“基础设施”

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * WebSocket配置类
 * 想象成给聊天室装上门、窗户和广播喇叭
 */
@Configuration
@EnableWebSocketMessageBroker  // 这句咒语的意思是:“芝麻开门,我要用WebSocket!”
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 配置消息代理,相当于设置聊天室的“广播站”
        config.enableSimpleBroker("/topic", "/queue");  // 简单内存代理
        
        // 设置应用程序的目的地前缀
        // 客户端发送消息到 /app/xxx,就像寄信要写“XX省XX市”
        config.setApplicationDestinationPrefixes("/app");
        
        // 用户私聊前缀(点对点)
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册WebSocket端点,相当于给聊天室安装“大门”
        registry.addEndpoint("/ws-chat")
                .setAllowedOriginPatterns("*")  // 允许所有来源(生产环境别这么干!)
                .withSockJS();  // 后备选项,万一浏览器太老,就降级使用HTTP长轮询
        
        // 再来一个不带SockJS的,给现代浏览器用
        registry.addEndpoint("/ws-chat")
                .setAllowedOriginPatterns("*");
        
        System.out.println("聊天室大门已安装!门牌号:/ws-chat");
        System.out.println("备用方案:SockJS已就位,IE6也能凑合用(大概吧)");
    }
}

第3步:消息控制器——聊天室的“主持人”

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 聊天控制器
 * 这位就是聊天室的主持人,负责喊:“XX说了一句话,大家快听!”
 */
@Controller
public class ChatController {
    
    private final SimpMessagingTemplate messagingTemplate;
    
    public ChatController(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
        System.out.println("聊天主持人已就位,话筒测试:喂喂喂~");
    }
    
    /**
     * 广播消息 - 大厅聊天
     * 客户端发送到:/app/chat.sendMessage
     * 服务端广播到:/topic/public
     */
    @MessageMapping("/chat.sendMessage")  // 接收消息的“信箱”
    @SendTo("/topic/public")  // 广播的“大喇叭”
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        // 给消息打个时间戳,就像聊天记录的时间标记
        chatMessage.setTimestamp(LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("HH:mm:ss")
        ));
        
        System.out.println("广播消息:" + chatMessage.getSender() + "说:" + chatMessage.getContent());
        
        // 如果是系统消息(比如有人加入/退出)
        if (chatMessage.getType() == MessageType.JOIN) {
            chatMessage.setContent(chatMessage.getSender() + " 闪亮登场!");
        } else if (chatMessage.getType() == MessageType.LEAVE) {
            chatMessage.setContent(chatMessage.getSender() + " 溜了溜了~ ");
        }
        
        return chatMessage;
    }
    
    /**
     * 用户加入 - 相当于进门喊一声“我来了!”
     */
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage,
                               SimpMessageHeaderAccessor headerAccessor) {
        
        // 在WebSocket会话中保存用户名,就像给用户发个“名牌”
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        
        chatMessage.setType(MessageType.JOIN);
        chatMessage.setTimestamp(LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("HH:mm:ss")
        ));
        
        System.out.println("新用户加入:" + chatMessage.getSender());
        System.out.println("当前在线人数:+1(我也不会算,大概吧)");
        
        return chatMessage;
    }
    
    /**
     * 私聊功能 - 偷偷说悄悄话
     * @param to 接收者用户名
     */
    @MessageMapping("/chat.private")
    public void privateMessage(@Payload ChatMessage chatMessage,
                               @Header("to") String toUser) {
        
        chatMessage.setTimestamp(LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("HH:mm:ss")
        ));
        
        System.out.println("私聊消息:" + chatMessage.getSender() + 
                          " 悄悄对 " + toUser + " 说:" + chatMessage.getContent());
        
        // 发送给特定用户:/user/{用户名}/queue/private
        messagingTemplate.convertAndSendToUser(
            toUser,
            "/queue/private",
            chatMessage
        );
        
        // 也发给自己,让自己看到发送的消息
        messagingTemplate.convertAndSendToUser(
            chatMessage.getSender(),
            "/queue/private",
            chatMessage
        );
    }
    
    /**
     * 消息模型类 - 聊天的“语言规范”
     */
    public static class ChatMessage {
        private MessageType type;      // 消息类型
        private String content;        // 消息内容
        private String sender;         // 发送者
        private String timestamp;      // 时间戳
        
        // 构造方法、getter、setter省略(但实际必须要有!)
        // 想象成:每个消息都要有信封、信纸、写信人、写信时间
    }
    
    /**
     * 消息类型枚举 - 聊天表情包分类
     */
    public enum MessageType {
        CHAT,   // 普通聊天
        JOIN,   // 加入
        LEAVE   // 离开
    }
}

第4步:前端实现——用户的“聊天界面”

<!-- chat.html -->
<!DOCTYPE html>
<html>
<head>
    <title>SpringBoot聊天室 - 禁止讨论为什么代码又报错</title>
    <style>
        body { font-family: 'Comic Sans MS', cursive; }
        #chat-container { 
            border: 3px solid #4CAF50; 
            border-radius: 15px;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
        }
        #message-area {
            height: 400px;
            overflow-y: auto;
            border: 2px dashed #ccc;
            padding: 10px;
            margin-bottom: 20px;
            background: white;
            border-radius: 10px;
        }
        .message { margin: 10px 0; padding: 10px; border-radius: 10px; }
        .my-message { background: #e3f2fd; text-align: right; }
        .their-message { background: #f1f8e9; }
        .system-message { 
            background: #fff3e0; 
            text-align: center;
            font-style: italic;
            color: #ff9800;
        }
        .join-message { color: #4CAF50; }
        .leave-message { color: #f44336; }
    </style>
</head>
<body>
    <div id="chat-container">
        <h1>SpringBoot聊天室</h1>
        <h3>当前状态:<span id="status">正在连接...</span></h3>
        
        <div id="connect-area">
            <input type="text" id="username" placeholder="取个霸气的昵称" />
            <button onclick="connect()" id="connect-btn">进入聊天室</button>
        </div>
        
        <div id="chat-area" style="display:none;">
            <div id="message-area"></div>
            
            <div>
                <input type="text" id="message-input" 
                       placeholder="说点什么吧..." 
                       style="width: 70%; padding: 10px;"
                       onkeypress="if(event.keyCode===13) sendMessage()" />
                <button onclick="sendMessage()" style="padding: 10px;">发送</button>
            </div>
            
            <div style="margin-top: 20px;">
                <input type="text" id="private-to" placeholder="私聊对象昵称" />
                <input type="text" id="private-message" placeholder="悄悄话内容" />
                <button onclick="sendPrivate()">发送悄悄话</button>
            </div>
        </div>
    </div>

    <!-- 引入WebSocket客户端库 -->
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    
    <script>
        let stompClient = null;
        let username = null;
        
        // 连接WebSocket - 相当于“敲门”
        function connect() {
            username = document.getElementById('username').value.trim();
            if (!username) {
                alert("请先取个昵称!不能叫'无名氏'吧?");
                return;
            }
            
            const socket = new SockJS('/ws-chat');
            stompClient = Stomp.over(socket);
            
            stompClient.connect({}, function(frame) {
                console.log("连接成功!服务器说:" + frame);
                document.getElementById('status').innerHTML = "已连接";
                document.getElementById('connect-area').style.display = 'none';
                document.getElementById('chat-area').style.display = 'block';
                
                // 订阅公共频道 - 相当于“坐在大厅听广播”
                stompClient.subscribe('/topic/public', function(message) {
                    showMessage(JSON.parse(message.body));
                });
                
                // 订阅私人频道 - 相当于“戴上耳机听悄悄话”
                stompClient.subscribe('/user/queue/private', function(message) {
                    const msg = JSON.parse(message.body);
                    msg.isPrivate = true;
                    showMessage(msg);
                });
                
                // 发送加入消息
                stompClient.send("/app/chat.addUser", {}, 
                    JSON.stringify({sender: username, type: 'JOIN'})
                );
            }, function(error) {
                console.log("连接失败:" + error);
                document.getElementById('status').innerHTML = "连接失败";
            });
        }
        
        // 发送消息 - 相当于“对着话筒喊话”
        function sendMessage() {
            const messageInput = document.getElementById('message-input');
            const content = messageInput.value.trim();
            
            if (content && stompClient) {
                const chatMessage = {
                    sender: username,
                    content: content,
                    type: 'CHAT'
                };
                
                stompClient.send("/app/chat.sendMessage", {}, 
                    JSON.stringify(chatMessage)
                );
                messageInput.value = '';
            }
        }
        
        // 发送私聊
        function sendPrivate() {
            const toUser = document.getElementById('private-to').value.trim();
            const content = document.getElementById('private-message').value.trim();
            
            if (toUser && content && stompClient) {
                stompClient.send("/app/chat.private", {to: toUser}, 
                    JSON.stringify({
                        sender: username,
                        content: content,
                        type: 'CHAT'
                    })
                );
                document.getElementById('private-message').value = '';
            }
        }
        
        // 显示消息 - 相当于“把话写在聊天记录上”
        function showMessage(message) {
            const messageArea = document.getElementById('message-area');
            const messageElement = document.createElement('div');
            
            messageElement.classList.add('message');
            
            // 根据不同消息类型设置样式
            if (message.isPrivate) {
                messageElement.innerHTML = `
                    <strong>${message.sender} 悄悄对你说:</strong>
                    <br/>${message.content}
                    <br/><small>${message.timestamp}</small>
                `;
                messageElement.style.background = '#fce4ec';
            } else if (message.type === 'JOIN') {
                messageElement.classList.add('system-message', 'join-message');
                messageElement.innerHTML = `${message.content}`;
            } else if (message.type === 'LEAVE') {
                messageElement.classList.add('system-message', 'leave-message');
                messageElement.innerHTML = `${message.content}`;
            } else if (message.sender === username) {
                messageElement.classList.add('my-message');
                messageElement.innerHTML = `
                    <strong>我:</strong>${message.content}
                    <br/><small>${message.timestamp}</small>
                `;
            } else {
                messageElement.classList.add('their-message');
                messageElement.innerHTML = `
                    <strong>${message.sender}:</strong>${message.content}
                    <br/><small>${message.timestamp}</small>
                `;
            }
            
            messageArea.appendChild(messageElement);
            messageArea.scrollTop = messageArea.scrollHeight;
        }
        
        // 页面关闭时发送离开消息
        window.addEventListener('beforeunload', function() {
            if (stompClient && username) {
                stompClient.send("/app/chat.sendMessage", {}, 
                    JSON.stringify({
                        sender: username,
                        type: 'LEAVE',
                        content: ''
                    })
                );
            }
        });
    </script>
</body>
</html>

第5步:进阶功能——让聊天室更“炫酷”

// 1. 在线用户管理
@Component
public class ChatUserService {
    private final Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
    
    public void userConnected(String username) {
        onlineUsers.add(username);
        broadcastOnlineUsers();
    }
    
    public void userDisconnected(String username) {
        onlineUsers.remove(username);
        broadcastOnlineUsers();
    }
    
    private void broadcastOnlineUsers() {
        messagingTemplate.convertAndSend("/topic/onlineUsers", onlineUsers);
    }
}

// 2. 消息持久化(保存聊天记录)
@Service
public class ChatMessageService {
    @Autowired
    private ChatMessageRepository repository;
    
    public void saveMessage(ChatMessage message) {
        repository.save(message);
        System.out.println("消息已保存到数据库,以后可以翻旧账了");
    }
    
    public List<ChatMessage> getRecentMessages() {
        return repository.findTop50ByOrderByTimestampDesc();
    }
}

// 3. 消息拦截器(敏感词过滤)
@Component
public class ChatInterceptor implements ChannelInterceptor {
    private final String[] sensitiveWords = {"密码", "银行卡", "V我50"};
    
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        String content = message.getPayload().toString();
        
        for (String word : sensitiveWords) {
            if (content.contains(word)) {
                System.out.println("检测到敏感词,消息已被吞掉");
                return null;  // 吞掉消息
            }
        }
        
        return message;
    }
}

三、总结:从“HTTP的尬聊”到“WebSocket的畅聊”

WebSocket的优势:

  1. 真正的双向通信:不再是“你问我答”,而是“畅所欲言”
  2. 低延迟:消息实时到达,不用等HTTP的“快递员”来回跑
  3. 减少带宽:一次握手,多次通信,省去了HTTP的“客套话”
  4. 更少的服务器压力:不用维护成千上万的轮询请求

SpringBoot集成WebSocket的核心思想:

  1. 配置是骨架@EnableWebSocketMessageBroker 是激活咒语
  2. 控制器是大脑@MessageMapping 指定消息接收点
  3. 消息代理是广播站SimpleBroker 负责消息分发
  4. STOMP是协议翻译官:把WebSocket的消息翻译成大家都能懂的语言

开发心得:

  1. 前端连接记住三步曲:SockJS创建连接 → Stomp封装协议 → 订阅/发送消息
  2. 后端开发记住三注解@MessageMapping(收信)、@SendTo(广播)、@Payload(取内容)
  3. 生产环境要加料:认证、授权、SSL、集群支持、监控指标...

最后:

从前,HTTP每次聊天都要重新握手,像极了社恐人士每次开口前都要心理建设半天。

现在,WebSocket一次握手终身连接,就像好哥们儿之间:“别废话,直接说!”

而SpringBoot就是那个贴心的管家,帮你把WebSocket的各种复杂配置都打包好,你只需要:

  1. 加个依赖(点杯咖啡)
  2. 写个配置类(摆好桌椅)
  3. 写个控制器(找个主持人)
  4. 前端连一下(客人入场)

然后就可以享受:高性能、低延迟、全双工的聊天体验

WebSocket:让HTTP的“尬聊”变成真正的“畅聊”.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海