[WebSocket入门]搭建WebSocket多人在线聊天室笔记(SpringBoot+WebSocket)
前言
使用websocket创建多人在线聊天室,本文参考了老外的一篇文章:https://www.callicoder.com/spring-boot-websocket-chat-example/
一、WebSocket是什么?
WebSocket是在单个TCP连接上进行全双工通信的协议,可以在服务器和客户端之间建立双向通信通道。
WebSocket 首先与服务器建立常规 HTTP 连接,然后通过发送Upgrade
标头将其升级为双向 WebSocket 连接。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
二、多人在线聊天室程序创建步骤
1、修改pom.xml文件
在springboot2程序中,在pom.xml添加websocket依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
2、WebSocket 配置
新建config文件夹,在文件夹下创建WebsocketConfig.java文件:
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;@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.setApplicationDestinationPrefixes("/app");registry.enableSimpleBroker("/topic");}
}
1、@EnableWebSocketMessageBroker:注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
2、通过WebSocketMessageBrokerConfigurer继承WebSocket消息代理的类,配置相关信息;在该类里面重载了两个方法:registerStompEndpoints()与configureMessageBroker()。
- 重载registerStompEndpoints()方法:
addEndpoint方法:添加STOMP协议的端点。这个HTTP URL是供WebSocket或SockJS客户端访问的地址;
withSockJS:指定端点使用SockJS协议
- 重载configureMessageBroker()方法:在该方法里设置简单消息代理,并配置消息的发送的地址符合配置的前缀的消息才发送到这个broker
registry.setApplicationDestinationPrefixes("/app"):全局(客户端)使用的消息前缀
3、创建 Message 实体
新建entity文件夹,在文件夹下创建实体类Message:
package com.chen.my_chat.entity;public class Message {public enum MessageType {CHAT,JOIN,LEAVE}private MessageType type;private String content;private String sender;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;}
}
在实体类Message中,type(枚举类型)表示消息类型,content表示消息内容,sender表示发送人。MessageType中的CHAT表示消息、JOIN表示加入、LEAVE表示离开。
4、创建Controller来接收和发送消息
创建MessageController.java文件,将负责从一个客户端接收消息,然后将其广播给其他客户端。
package com.chen.my_chat.controller;import com.chen.my_chat.entity.Message;
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.stereotype.Controller;@Controller
public class MessageController {@MessageMapping("/chat.sendMessage")@SendTo("/topic/public")public Message sendMessage(@Payload Message message){return message;}public Message addUser(@Payload Message message,SimpMessageHeaderAccessor headerAccessor){//把用户添加到websocket sessionheaderAccessor.getSessionAttributes().put("username",message.getSender());return message;}}
@SendTo:会将接收到的消息发送到指定的路由目的地,所有订阅该消息的用户都能收到,属于广播。
5、添加WebSocket事件监听
创建listener文件夹,在文件夹下添加WebsocketEventListener.java文件:
package com.chen.my_chat.listener;import com.chen.my_chat.entity.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;import javax.annotation.Resource;@Component
public class WebsocketEventListener {public static final Logger logger = LoggerFactory.getLogger(WebsocketEventListener.class);@Resourceprivate SimpMessageSendingOperations messageSendingOperations;@EventListenerpublic void handleWebSocketConnectListener(SessionConnectEvent event){logger.info("接收到一个新的websocket连接");}@EventListenerpublic void handleWebSocketDisConnectListener(SessionConnectEvent event){StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());String username = (String) headerAccessor.getSessionAttributes().get("username");if (username != null){logger.info("用户未连接:" + username);Message message = new Message();message.setType(Message.MessageType.LEAVE);message.setSender(username);messageSendingOperations.convertAndSend("/topic/public",message);}}}
6、创建前端网页
index.html:
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"><title>Spring Boot WebSocket Chat Application</title><link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<noscript><h2>Sorry! Your browser doesn't support Javascript</h2>
</noscript><div id="username-page"><div class="username-page-container"><h1 class="title">Type your username</h1><form id="usernameForm" name="usernameForm"><div class="form-group"><input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control" /></div><div class="form-group"><button type="submit" class="accent username-submit">Start Chatting</button></div></form></div>
</div><div id="chat-page" class="hidden"><div class="chat-container"><div class="chat-header"><h2>Spring WebSocket Chat Demo</h2></div><div class="connecting">Connecting...</div><ul id="messageArea"></ul><form id="messageForm" name="messageForm"><div class="form-group"><div class="input-group clearfix"><input type="text" id="message" placeholder="Type a message..." autocomplete="off" class="form-control"/><button type="submit" class="primary">Send</button></div></div></form></div>
</div><script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
main.js:
'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 TopicstompClient.subscribe('/topic/public', onMessageReceived);// Tell your username to the serverstompClient.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)
main.css:
* {-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;
}html,body {height: 100%;overflow: hidden;
}body {margin: 0;padding: 0;font-weight: 400;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;font-size: 1rem;line-height: 1.58;color: #333;background-color: #f4f4f4;height: 100%;
}body:before {height: 50%;width: 100%;position: absolute;top: 0;left: 0;background: #128ff2;content: "";z-index: 0;
}.clearfix:after {display: block;content: "";clear: both;
}.hidden {display: none;
}.form-control {width: 100%;min-height: 38px;font-size: 15px;border: 1px solid #c8c8c8;
}.form-group {margin-bottom: 15px;
}input {padding-left: 10px;outline: none;
}h1, h2, h3, h4, h5, h6 {margin-top: 20px;margin-bottom: 20px;
}h1 {font-size: 1.7em;
}a {color: #128ff2;
}button {box-shadow: none;border: 1px solid transparent;font-size: 14px;outline: none;line-height: 100%;white-space: nowrap;vertical-align: middle;padding: 0.6rem 1rem;border-radius: 2px;transition: all 0.2s ease-in-out;cursor: pointer;min-height: 38px;
}button.default {background-color: #e8e8e8;color: #333;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
}button.primary {background-color: #128ff2;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);color: #fff;
}button.accent {background-color: #ff4743;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);color: #fff;
}#username-page {text-align: center;
}.username-page-container {background: #fff;box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);border-radius: 2px;width: 100%;max-width: 500px;display: inline-block;margin-top: 42px;vertical-align: middle;position: relative;padding: 35px 55px 35px;min-height: 250px;position: absolute;top: 50%;left: 0;right: 0;margin: 0 auto;margin-top: -160px;
}.username-page-container .username-submit {margin-top: 10px;
}#chat-page {position: relative;height: 100%;
}.chat-container {max-width: 700px;margin-left: auto;margin-right: auto;background-color: #fff;box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);margin-top: 30px;height: calc(100% - 60px);max-height: 600px;position: relative;
}#chat-page ul {list-style-type: none;background-color: #FFF;margin: 0;overflow: auto;overflow-y: scroll;padding: 0 20px 0px 20px;height: calc(100% - 150px);
}#chat-page #messageForm {padding: 20px;
}#chat-page ul li {line-height: 1.5rem;padding: 10px 20px;margin: 0;border-bottom: 1px solid #f4f4f4;
}#chat-page ul li p {margin: 0;
}#chat-page .event-message {width: 100%;text-align: center;clear: both;
}#chat-page .event-message p {color: #777;font-size: 14px;word-wrap: break-word;
}#chat-page .chat-message {padding-left: 68px;position: relative;
}#chat-page .chat-message i {position: absolute;width: 42px;height: 42px;overflow: hidden;left: 10px;display: inline-block;vertical-align: middle;font-size: 18px;line-height: 42px;color: #fff;text-align: center;border-radius: 50%;font-style: normal;text-transform: uppercase;
}#chat-page .chat-message span {color: #333;font-weight: 600;
}#chat-page .chat-message p {color: #43464b;
}#messageForm .input-group input {float: left;width: calc(100% - 85px);
}#messageForm .input-group button {float: left;width: 80px;height: 38px;margin-left: 5px;
}.chat-header {text-align: center;padding: 15px;border-bottom: 1px solid #ececec;
}.chat-header h2 {margin: 0;font-weight: 500;
}.connecting {padding-top: 5px;text-align: center;color: #777;position: absolute;top: 65px;width: 100%;
}@media screen and (max-width: 730px) {.chat-container {margin-left: 10px;margin-right: 10px;margin-top: 10px;}
}@media screen and (max-width: 480px) {.chat-container {height: calc(100% - 30px);}.username-page-container {width: auto;margin-left: 15px;margin-right: 15px;padding: 25px;}#chat-page ul {height: calc(100% - 120px);}#messageForm .input-group button {width: 65px;}#messageForm .input-group input {width: calc(100% - 70px);}.chat-header {padding: 10px;}.connecting {top: 60px;}.chat-header h2 {font-size: 1.1em;}
}
总结
本文基于Websocket特性搭建了一个简易的多人在线聊天室。