使用 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 版本出来。感谢 🙏

相关推荐
ღ᭄ꦿ࿐Never say never꧂1 分钟前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴11 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
.生产的驴11 分钟前
SpringBoot 消息队列RabbitMQ在代码中声明 交换机 与 队列使用注解创建
java·spring boot·分布式·servlet·kafka·rabbitmq·java-rabbitmq
海里真的有鱼19 分钟前
Spring Boot 中整合 Kafka
后端
布瑞泽的童话25 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
听封28 分钟前
Thymeleaf 的创建
java·spring boot·spring·maven
写bug写bug34 分钟前
6 种服务限流的实现方式
java·后端·微服务
白鹭凡37 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867841 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏