1. 前言:
SpringBoot+websocket的实现其实不难,你可以使用原生的实现,也就是websocket本身的OnOpen、OnClosed等等这样的注解来实现,以及对WebSocketHandler的实现,类似于netty的那种使用方式,而且原生的还提供了对websocket的监听,服务端能更好的控制及统计(即上文实现的方式)。
但是,真实项目中还是使用Stomp实现的居多,因为独立服务更方便,便于后期搭建集群环境做横向扩展,且内置的方法也很简单,既然如此,我们还是以主流实现方式为准来学习吧。
2. stomp
当直接使用WebSocket时(或SockJS)就很类似于使用TCP套接字来编写Web应用。因为没有高层级的线路协议(wire protocol),因此就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。
就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。
与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:
>>> SEND
transaction:tx-0
destination:/app/marco
content-length:20
{"message":"Marco!"}
3. 需求:
-
登陆:
-
1号用户加入,发送消息:
-
2号:
-
3号:
-
退出:
-
要求使用stomp完成
4. websocket配置:
// websocket核心配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注册stomp端点
* @param registry stomp端点注册对象
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 配置stomp端点地址
registry.addEndpoint("/ws").withSockJS();
}
/**
* 配置消息代理
* @param registry 消息代理注册对象
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义客户端访问服务端消息接口时的前缀
registry.setApplicationDestinationPrefixes("/app");
// 配置服务端推送消息给客户端的代理路径
registry.enableSimpleBroker("/topic");
// 定义点对点推送时的前缀为/queue
registry.setUserDestinationPrefix("/queue");
// Use this for enabling a Full featured broker like RabbitMQ
/*
registry.enableStompBrokerRelay("/topic")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest");
*/
}
}
其中:
-
@EnableWebSocketMessageBroker
:用于开启stomp协议,这样就能支持@MessageMapping注解,类似于@requestMapping一样,同时前端可以使用Stomp客户端进行通讯; -
WebSocketMessageBrokerConfigurer
接口:实现了其中的两个方法:-
registerStompEndpoints
实现:- 注册一个websocket端点,客户端将使用它连接到我们的websocket服务器。
- withSockJS()是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。
- 方法名中的STOMP是来自Spring框架STOMP实现。 STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能
-
configureMessageBroker
实现:主要用来设置客户端订阅消息的路径(可以多个)、点对点订阅路径前缀的设置、访问服务端@MessageMapping接口的前缀路径、心跳设置等;- 第一行定义了以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。
- 第二行定义了以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息
-
5. model对象:
public class ChatMessage {
private MessageType type;
private String content;
private String sender;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
public MessageType getType() {
return type;
}
public void setType(MessageType type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
@Override
public String toString() {
return "ChatMessage{" +
"type=" + type +
", content='" + content + '\'' +
", sender='" + sender + '\'' +
'}';
}
}
6. controller接收和发送消息:
/**
* 发送广播消息
* -- 说明:
* 1)、@MessageMapping注解对应客户端的stomp.send('url');
* 2)、用法一:要么配合@SendTo("转发的订阅路径"),去掉messagingTemplate,同时return msg来使用,return msg会去找@SendTo注解的路径;
* 3)、用法二:要么设置成void,使用messagingTemplate来控制转发的订阅路径,且不能return msg
*/
@Controller
@Slf4j
public class ChatController {
// 实现向浏览器发送消息的功能
private final SimpMessagingTemplate messagingTemplate;
public ChatController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// 用法一:
@MessageMapping("/send")
public void sendAll(@RequestParam String msg) {
log.info("[发送消息]>>>> msg: {}", msg);
// 发送消息给客户端
// 第一个参数是浏览器中订阅消息的地址,第二个参数是消息本身
messagingTemplate.convertAndSend("/topic/public", msg);
}
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage;
}
// 用法二:
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
// Add username in web socket session
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage;
}
}
- 消息接口使用@MessageMapping注解,前面讲的配置类@EnableWebSocketMessageBroker注解开启后才能使用这个
- 我们在websocket配置中,以/app开头的客户端发送的所有消息都将路由到这些使用@MessageMapping注释的消息处理方法。
例如,具有目标/app/chat.sendMessage的消息将路由到sendMessage()方法,并且具有目标/app/chat.addUser的消息将路由到addUser()方法 - 这里稍微提一下,真正线上项目都是把websocket服务做成单独的网关形式,提供rest接口给其他服务调用,达到共用的目的,本项目因为不涉及任何数据库交互,所以直接用@MessageMapping注解,后续完整IM项目接入具体业务后会做一个独立的websocket服务
- 发送消息的两个用法:
- 用法一:要么配合@SendTo(“转发的订阅路径”),去掉messagingTemplate,同时return msg来使用,return msg会去找@SendTo注解的路径;
- 用法二:要么设置成void,使用messagingTemplate来控制转发的订阅路径,且不能return msg
7. 添加websocket监听事件
完成了上述代码后,我们还需要对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。
@Component
public class WebSocketEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
logger.info("Received a new web socket connection");
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String username = (String) headerAccessor.getSessionAttributes().get("username");
if(username != null) {
logger.info("User Disconnected : " + username);
ChatMessage chatMessage = new ChatMessage();
chatMessage.setType(ChatMessage.MessageType.LEAVE);
chatMessage.setSender(username);
messagingTemplate.convertAndSend("/topic/public", chatMessage);
}
}
}
我们已经在ChatController中定义的addUser()方法中广播了用户加入事件。因此,我们不需要在SessionConnected事件中执行任何操作。
在SessionDisconnect事件中,编写代码用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。
8. 主要前端代码:
'use strict';
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
'#2196F3', '#32c787', '#00BCD4', '#ff5652',
'#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
function connect(event) {
username = document.querySelector('#name').value.trim();
if(username) {
usernamePage.classList.add('hidden');
chatPage.classList.remove('hidden');
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
event.preventDefault();
}
function onConnected() {
// Subscribe to the Public Topic
stompClient.subscribe('/topic/public', onMessageReceived);
// Tell your username to the server
stompClient.send("/app/chat.addUser",
{},
JSON.stringify({sender: username, type: 'JOIN'})
)
connectingElement.classList.add('hidden');
}
function onError(error) {
connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
connectingElement.style.color = 'red';
}
function sendMessage(event) {
var messageContent = messageInput.value.trim();
if(messageContent && stompClient) {
var chatMessage = {
sender: username,
content: messageInput.value,
type: 'CHAT'
};
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault();
}
function onMessageReceived(payload) {
var message = JSON.parse(payload.body);
var messageElement = document.createElement('li');
if(message.type === 'JOIN') {
messageElement.classList.add('event-message');
message.content = message.sender + ' joined!';
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
message.content = message.sender + ' left!';
} else {
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(message.sender[0]);
avatarElement.appendChild(avatarText);
avatarElement.style['background-color'] = getAvatarColor(message.sender);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var textElement = document.createElement('p');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
var hash = 0;
for (var i = 0; i < messageSender.length; i++) {
hash = 31 * hash + messageSender.charCodeAt(i);
}
var index = Math.abs(hash % colors.length);
return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)
代码解释:文章来源:https://www.toymoban.com/news/detail-402719.html
- connect()函数使用SockJS和stomp客户端连接到我们在Spring Boot中配置的/ws端点。
- 成功连接后,客户端订阅/topic/public,并通过向/app/chat.addUser目的地发送消息将该用户的名称告知服务器。
- stompClient.subscribe()函数采用一种回调方法,只要消息到达订阅主题,就会调用该方法。
- 其它的代码用于在屏幕上显示和格式化消息。
参考:
https://blog.csdn.net/xiangyangsanren/article/details/123970860
https://blog.csdn.net/qqxx6661/article/details/98883166
https://blog.csdn.net/qq_53021672/article/details/124313430
https://blog.csdn.net/qq_35387940/article/details/108276136(最全的)文章来源地址https://www.toymoban.com/news/detail-402719.html
到了这里,关于WebSocket(三) -- 使用websocket+stomp实现群聊功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!