使用 SpringBoot WebSocket 和 Next.js 搭建聊天室

最近在项目中需要用到 WebSocket 进行通信。于是简单学习了一下,并且搭建了一个简易的聊天室,在此记录下并分享。

后端使用了 SpringBoot 集成的 WebSocket,前端使用了 Next.js。

PS:每一步都贴了详细的代码和注释,复制到项目里就能跑起来~

什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

Websocket 的优点在于:

  • 可以与任何 Web 浏览器一起使用
  • 传递二进制数据支持 JSON,XML 等格式
  • 具有较低的延迟,从而可以实现更快的通信
  • 在客户端和服务器之间保持长时间的连接,从而可以减少 HTTP 请求的数量。

SpringBoot 整合 WebSocket

话不多说,直接上代码。

首先创建 SpringBoot 项目,引入 WebSocket 依赖。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

创建 WebSocketConfig:

java 复制代码
@Configuration
@EnableWebSocketMessageBroker
@CrossOrigin(origins = "http://localhost:3000")
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  // 配置WebSocket的端点,即客户端将连接到的端点
  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws").setAllowedOrigins("http://localhost:3000");
  }

  // 配置消息代理,负责将消息从一个端点路由到另一个端点
  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 指定应用程序消息的前缀
    registry.setApplicationDestinationPrefixes("/app");
    // 启用一个简单的消息代理,用于向客户端广播消息到特定的主题(topic)
    registry.enableSimpleBroker("/topic");
  }
}

定义 Message 类和 MessageType,用来表示消息类型。

java 复制代码
public class ChatMessage {
  private String content;
  private String sender;
  private MessageType type;
}

public enum MessageType {
  CHAT,
  JOIN,
  LEAVE
}

创建 Controller:

java 复制代码
@Controller
public class ChatController {

  // 当客户端发送消息到 "/chat.sendMessage" 时,将消息广播到 "/topic/public"
  @MessageMapping("/chat.sendMessage")
  @SendTo("/topic/public")
  public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
    // 将收到的消息原样返回,用于广播给所有订阅 "/topic/public" 的客户端
    return chatMessage;
  }

  // 当客户端发送消息到 "/chat.addUser" 时,将用户加入聊天,并广播到 "/topic/public"
  @MessageMapping("/chat.addUser")
  @SendTo("/topic/public")
  public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {
    // 将用户添加到会话属性中,以便识别用户
    headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
    // 将加入聊天的消息广播给所有订阅 "/topic/public" 的客户端
    return chatMessage;
  }
}

创建一个事件监听器,用来处理会话断开连接的事件。

java 复制代码
@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketEventListener {

  // 用于向客户端发送消息的操作类
  private final SimpMessageSendingOperations messageSendingOperations;

  // 监听 WebSocket 会话断开连接的事件
  @EventListener
  public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
    // 从事件中获取 StompHeaderAccessor,用于处理 Stomp 消息头
    StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

    // 从会话属性中获取用户名
    String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username");

    // 如果用户名不为空,表示用户已经加入聊天
    if (username != null) {
      // 记录用户断开连接的日志
      log.info("User disconnected: {}", username);

      // 创建一个消息,表示用户离开聊天
      var chatMessage = ChatMessage.builder()
          .type(MessageType.LEAVE)
          .sender(username)
          .build();

      // 将离开聊天的消息广播到 "/topic/public",通知其他在线用户
      messageSendingOperations.convertAndSend("/topic/public", chatMessage);
    }
  }
}

服务端就创建完成了,接下来创建前端。

前端连接 WebSocket

创建一个 Next.js 项目,然后安装 @stomp/stompjs 包。

bash 复制代码
npm i @stomp/stompjs

前端代码比较简单,为了演示,我把代码放在了同一个文件中:

tsx 复制代码
'use client';

import { Client, StompConfig } from '@stomp/stompjs';
import { useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';

type Message = {
  content: string;
  sender: string;
  type: 'JOIN' | 'CHAT' | 'LEAVE';
};

const stompConfig: StompConfig = {
  brokerURL: 'ws://localhost:8080/ws',
  debug: function (str: string) {
    console.log('STOMP: ' + str);
  },
  reconnectDelay: 200,
};

const ChatPage = () => {
  const searchParams = useSearchParams();
  const username = searchParams.get('username');

  const [stompClient, setStompClient] = useState(() => new Client(stompConfig));
  const [messageList, setMessageList] = useState<Message[]>([]);
  const [message, setMessage] = useState('');

  const handleMessage = useCallback((msg: any) => {
    const newMessage = JSON.parse(msg.body);
    setMessageList((prevMessages) => [...prevMessages, newMessage]);
  }, []);

  const handleSendMessage = () => {
    const chatMessage = {
      sender: username,
      content: message,
      type: 'CHAT',
    };
    stompClient.publish({
      destination: '/app/chat.sendMessage',
      body: JSON.stringify(chatMessage),
    });
    setMessage('');
  };

  const handleInputChange = (e: any) => {
    setMessage(e.target.value);
  };

  useEffect(() => {
    stompClient.activate();
    stompClient.onConnect = () => {
      stompClient.publish({
        destination: '/app/chat.addUser',
        body: JSON.stringify({ sender: username, type: 'JOIN' }),
      });
      stompClient?.subscribe('/topic/public', handleMessage);
    };

    return () => {
      stompClient?.deactivate();
    };
  }, [stompClient, handleMessage, username]);

  return (
    <div className="flex flex-col max-w-xl h-screen m-auto">
      <h1 className="text-2xl font-medium text-center my-4">Spring Boot Websocket Chat Demo</h1>
      <div className="flex-grow px-4">
        <div className="border-t">
          {messageList.map((message, index) => (
            <div key={index} className="py-3">
              {message.type === 'JOIN' && (
                <p className="text-center text-gray-400 text-sm">{`${
                  message.sender === username ? '你' : message.sender
                } 加入了聊天室`}</p>
              )}
              {message.type === 'CHAT' && (
                <div className={`${message.sender === username ? 'text-right' : 'text-left'} mb-2`}>
                  <p className="text-sm font-bold">{message.sender === username ? '你' : message.sender}</p>
                  <p className="bg-blue-200 rounded p-2 inline-block">{message.content}</p>
                </div>
              )}
              {message.type === 'LEAVE' && (
                <p className="text-center text-gray-400 text-sm">{`${message.sender} 离开了聊天室`}</p>
              )}
            </div>
          ))}
        </div>
      </div>
      <div className="m-4 flex items-center">
        <input
          type="text"
          value={message}
          onChange={handleInputChange}
          className="border rounded px-3 py-2 mr-2 focus:outline-none focus:ring focus:border-blue-300 flex-grow"
          placeholder="Type your message..."
        />
        <button
          onClick={handleSendMessage}
          className="bg-blue-500 text-white px-4 py-2 rounded transition duration-300 hover:bg-blue-600 focus:outline-none focus:ring focus:border-blue-300"
        >
          Send
        </button>
      </div>
    </div>
  );
};

export default ChatPage;

测试运行效果

启动项目,访问 http://localhost:3000/chat?username=xxx

这里依次打开三个浏览器窗口:

bash 复制代码
http://localhost:3000/chat?username=喜羊羊
http://localhost:3000/chat?username=美羊羊
http://localhost:3000/chat?username=沸羊羊

通过查看浏览器的控制台,可以看到输出信息,主要是 STOMP 打印的 debug 日志,说明连接成功了。

下面是运行效果:

通过最左侧的窗口可以看到,每当有新用户加入,每个窗口都会显示出一条信息。之后在任意窗口中发送信息,也会广播给所有用户。

最后,因为我们实现了 handleWebSocketDisconnectListener,在用户断开连接时,也会向所有用户发送消息,效果如下:

总结

Spring Boot 整合 WebSocket 还是比较简单的,只需要引入 Spring Boot 的 WebSocket 依赖,并创建 WebSocketConfig 和 ChatController 类,一个最基本的功能就实现了。

客户端可以使用 @stomp/stompjs 包来连接 WebSocket 服务,通过订阅和发布消息实现前后端的实时通信。

当然本文中的实例只是一个很简单的版本,并不支持消息存储、私聊等功能,如果大家有兴趣,可以点赞或留言,我会弄个 v2 版本出来。感谢 🙏

相关推荐
xjt_09018 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
rannn_11111 分钟前
【苍穹外卖|Day4】套餐页面开发(新增套餐、分页查询、删除套餐、修改套餐、起售停售)
java·spring boot·后端·学习
qq_124987075315 分钟前
基于JavaWeb的大学生房屋租赁系统(源码+论文+部署+安装)
java·数据库·人工智能·spring boot·计算机视觉·毕业设计·计算机毕业设计
我是伪码农20 分钟前
Vue 2.3
前端·javascript·vue.js
短剑重铸之日21 分钟前
《设计模式》第十一篇:总结
java·后端·设计模式·总结
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
倒流时光三十年1 小时前
SpringBoot 数据库同步 Elasticsearch 性能优化
数据库·spring boot·elasticsearch
码农小卡拉1 小时前
深入解析Spring Boot文件加载顺序与加载方式
java·数据库·spring boot
Dragon Wu1 小时前
Spring Security Oauth2.1 授权码模式实现前后端分离的方案
java·spring boot·后端·spring cloud·springboot·springcloud
一个有梦有戏的人2 小时前
Python3基础:进阶基础,筑牢编程底层能力
后端·python