与Spring Boot和WebSockets聊天





. « Spring Framework» . , , : « Spring», .









使用“服务器发送事件”和“ Redis”构建类似于Facebook的可扩展通知中,我们使用了“服务器发送事件”将消息从服​​务器发送到客户端。它还提到了WebSocket,这是服务器和客户端之间的双向通信技术。



在本文中,我们将介绍WebSocket的一种常见用例。我们将编写一个私人消息传递应用程序。



以下视频演示了我们将要做的事情。





WebSockets和STOMP简介



WebSocket是用于服务器和客户端之间双向通信的协议。

WebSocket与HTTP不同,应用层协议是一种传输层协议(TCP)。尽管HTTP用于初始连接,但是该连接随后被“升级”为WebSocket中使用的TCP连接。



WebSocket是不定义消息格式的低级协议。因此,WebSocket RFC定义了描述消息结构和标准的子协议。我们将在WebSockets上使用STOMP(在WebSockets上使用STOMP)。



协议STOMP(面向消息的文本的简单/流式传输协议)定义了服务器与客户端之间的消息交换规则。



STOMP与HTTP相似,并且使用以下命令在TCP之上运行:



  • 连接
  • 订阅
  • 取消订阅
  • 发送
  • 开始
  • 承诺
  • 确认


可以在此处找到STOMP命令的规范和完整列表



建筑







  • 验证服务是负责认证和管理用户。我们不会在这里重新发明轮子,而是将使用JWT的身份验证服务和使用Spring Boot的Social Authentication
  • 聊天服务是负责配置的WebSocket,处理STOMP消息,以及存储和处理用户的消息。
  • 聊天客户端是使用STOMP客户端连接并订阅聊天ReactJS应用。这里也是用户界面。


讯息模型



首先要考虑的是消息模型。ChatMessage看起来像这样:



public class ChatMessage {
   @Id
   private String id;
   private String chatId;
   private String senderId;
   private String recipientId;
   private String senderName;
   private String recipientName;
   private String content;
   private Date timestamp;
   private MessageStatus status;
}


该类ChatMessage非常简单,其中包含用于标识发件人和收件人的字段。



它还具有一个状态字段,指示该消息是否已传递到客户端。



public enum MessageStatus {
    RECEIVED, DELIVERED
}


当服务器从聊天中收到消息时,它不会直接将消息发送给收件人,而是会发送通知(ChatNotification)通知客户端已收到新消息。此后,客户自己可以收到一条新消息。客户端一收到消息,便将其标记为“已发送”。



通知如下所示:



public class ChatNotification {
    private String id;
    private String senderId;
    private String senderName;
}


通知包含新消息的ID和有关发件人的信息,以便客户端可以显示有关新消息的信息或新消息的数量,如下所示。











在Spring中配置WebSocket和STOMP



第一步是配置STOMP端点和消息代理。



为此,我们使用注解创建一个WebSocketConfig@Configuration@EnableWebSocketMessageBroker



@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker( "/user");
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry
                .addEndpoint("/ws")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
        resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setObjectMapper(new ObjectMapper());
        converter.setContentTypeResolver(resolver);
        messageConverters.add(converter);
        return false;
    }
}


第一种方法使用一个带前缀的地址配置一个简单的内存中消息代理,/user以发送和接收消息。前缀地址/app用于通过带注释的方法处理的消息@MessageMapping,我们将在下一节中讨论。



第二种方法注册STOMP端点/ws客户端将使用此终结点连接到STOMP服务器。这还包括一个后备SockJS,如果WebSocket不可用,则将使用它。



最后一种方法配置Spring用来将消息转换为JSON或从JSON转换的JSON转换器。



处理消息的控制器



在本节中,我们将创建一个处理请求控制器它将接收来自用户的消息并将其发送给收件人。



@Controller
public class ChatController {

    @Autowired private SimpMessagingTemplate messagingTemplate;
    @Autowired private ChatMessageService chatMessageService;
    @Autowired private ChatRoomService chatRoomService;

    @MessageMapping("/chat")
    public void processMessage(@Payload ChatMessage chatMessage) {
        var chatId = chatRoomService
                .getChatId(chatMessage.getSenderId(), chatMessage.getRecipientId(), true);
        chatMessage.setChatId(chatId.get());

        ChatMessage saved = chatMessageService.save(chatMessage);
        
        messagingTemplate.convertAndSendToUser(
                chatMessage.getRecipientId(),"/queue/messages",
                new ChatNotification(
                        saved.getId(),
                        saved.getSenderId(),
                        saved.getSenderName()));
    }
}


借助注释,@MessageMapping我们配置将消息发送到/app/chat方法时调用processMessage请注意,先前配置的应用程序前缀将添加到映射中/app



此方法将消息存储在MongoDB中,然后调用该方法convertAndSendToUser以将通知发送到目标。在地址



convertAndSendToUser添加前缀/user的方法最终地址将如下所示:recipientId/queue/messages



/user/{recipientId}/queue/messages


该地址的所有订户(在我们的示例中为一个)将收到该消息。



产生chatId



对于两个用户之间的每次对话,我们创建一个聊天室并生成一个唯一的聊天室以进行识别chatIdChatRoom



如下所示:



public class ChatRoom {
    private String id;
    private String chatId;
    private String senderId;
    private String recipientId;
}


该值chatId等于串联senderId_recipientId对于每个会话,我们保留两个相同的实体chatId:一个在发送者和接收者之间,另一个在接收者和发送者之间,以便两个用户都相同chatId



JavaScript客户端



在本部分中,我们将创建一个JavaScript客户端,该客户端将向该客户端发送消息并从中接收WebSocket / STOMP服务器。



我们将使用SockJS和Stomp.js通过WebSocket通过STOMP与服务器进行通信。



const connect = () => {
    const Stomp = require("stompjs");
    var SockJS = require("sockjs-client");
    SockJS = new SockJS("http://localhost:8080/ws");
    stompClient = Stomp.over(SockJS);
    stompClient.connect({}, onConnected, onError);
  };


该方法connect()建立与的连接/ws,我们的服务器正在其中等待连接,还定义了一个回调函数onConnected,该函数将在成功连接后onError调用,并在连接到服务器时发生错误时调用。



const onConnected = () => {
    console.log("connected");

    stompClient.subscribe(
      "/user/" + currentUser.id + "/queue/messages",
      onMessageReceived
    );
  };


该方法onConnected()订阅特定的地址并接收在那里发送的所有消息。



const sendMessage = (msg) => {
    if (msg.trim() !== "") {
      const message = {
        senderId: currentUser.id,
        recipientId: activeContact.id,
        senderName: currentUser.name,
        recipientName: activeContact.name,
        content: msg,
        timestamp: new Date(),
      };
        
      stompClient.send("/app/chat", {}, JSON.stringify(message));
    }
  };


在方法的最后,一条sendMessage()消息被发送到/app/chat我们的控制器中指定的地址



结论



在本文中,我们介绍了使用Spring Boot和基于WebSocket的STOMP创建聊天的所有要点。



我们还使用SockJs和Stomp.js库构建了一个JavaScript客户端



示例源代码可以在这里找到






了解有关该课程的更多信息。











All Articles