springboot+vue+websocket+redis实现单聊代码、存储历史消息
时间: 2023-08-28 22:04:36 浏览: 381
这里提供一个简单的示例代码,实现了Spring Boot和Vue.js的单聊功能,使用WebSocket进行实时通信,并使用Redis存储历史消息。
后端代码(Spring Boot):
1. 依赖:
```xml
<dependencies>
<!-- Spring Boot Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
```
2. 配置文件:
```yml
spring:
redis:
host: localhost
port: 6379
logging:
level:
org.springframework.web.socket: DEBUG
```
3. 实体类:
```java
public class Message {
private String from;
private String to;
private String content;
private Date time;
// getters and setters
}
```
4. WebSocket配置:
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/chat").setAllowedOrigins("*");
}
}
```
5. WebSocket处理器:
```java
@Component
public class WebSocketHandler extends TextWebSocketHandler {
@Autowired
private RedisTemplate<String, Message> redisTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
private static final String KEY_PREFIX = "chat:";
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 获取当前用户
String user = (String) session.getAttributes().get("user");
// 订阅Redis频道
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.subscribe(new MessageListener(), KEY_PREFIX + user);
return null;
}
});
// 发送历史消息
List<Message> messages = redisTemplate.opsForList().range(KEY_PREFIX + user, 0, -1);
if (messages != null && messages.size() > 0) {
for (Message message : messages) {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
}
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 获取当前用户
String user = (String) session.getAttributes().get("user");
// 解析消息
Message msg = objectMapper.readValue(message.getPayload(), Message.class);
msg.setFrom(user);
msg.setTime(new Date());
// 存储到Redis
redisTemplate.opsForList().rightPush(KEY_PREFIX + msg.getTo(), msg);
// 发送给对方
WebSocketSession targetSession = sessions.get(msg.getTo());
if (targetSession != null && targetSession.isOpen()) {
targetSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(msg)));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 获取当前用户
String user = (String) session.getAttributes().get("user");
// 取消订阅Redis频道
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.unsubscribe(KEY_PREFIX + user);
return null;
}
});
}
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
private class MessageListener implements MessageListenerAdapter {
@Override
public void onMessage(Message message, byte[] pattern) {
WebSocketSession session = sessions.get(message.getTo());
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
```
6. 控制器:
```java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private RedisTemplate<String, Message> redisTemplate;
@PostMapping("/send")
public void send(@RequestBody Message message) {
// 存储到Redis
redisTemplate.opsForList().rightPush("chat:" + message.getFrom(), message);
redisTemplate.opsForList().rightPush("chat:" + message.getTo(), message);
// 发布消息
redisTemplate.convertAndSend("chat:" + message.getTo(), message);
}
@GetMapping("/history")
public List<Message> history(String user1, String user2) {
String key = "chat:" + user1 + ":" + user2;
List<Message> messages = redisTemplate.opsForList().range(key, 0, -1);
Collections.reverse(messages);
return messages;
}
}
```
前端代码(Vue.js):
1. 依赖:
```html
<script src="/js/vue.min.js"></script>
<script src="/js/sockjs.min.js"></script>
<script src="/js/stomp.min.js"></script>
<script src="/js/lodash.min.js"></script>
```
2. HTML:
```html
<div id="app">
<div>
<label>当前用户:</label>
<select v-model="currentUser" @change="connect">
<option v-for="user in users" :value="user">{{ user }}</option>
</select>
</div>
<div v-if="connected">
<div>
<label>对方用户:</label>
<input v-model="otherUser">
</div>
<div>
<textarea v-model="message"></textarea>
<button @click="send">发送</button>
</div>
<div>
<ul>
<li v-for="msg in messages">{{ msg.from }} -> {{ msg.to }}: {{ msg.content }}</li>
</ul>
</div>
</div>
</div>
```
3. JavaScript:
```javascript
var app = new Vue({
el: '#app',
data: {
users: ['user1', 'user2', 'user3'],
currentUser: 'user1',
otherUser: '',
message: '',
connected: false,
messages: []
},
methods: {
connect: function () {
var self = this;
if (self.stompClient != null) {
self.stompClient.disconnect();
}
var socket = new SockJS('/chat');
self.stompClient = Stomp.over(socket);
self.stompClient.connect({}, function () {
self.stompClient.subscribe('/user/queue/messages', function (msg) {
var message = JSON.parse(msg.body);
self.messages.push(message);
});
self.connected = true;
}, function (error) {
console.log(error);
});
},
send: function () {
var self = this;
var message = {
from: self.currentUser,
to: self.otherUser,
content: self.message
};
self.stompClient.send('/app/chat/send', {}, JSON.stringify(message));
self.message = '';
},
loadHistory: function () {
var self = this;
axios.get('/api/chat/history', {
params: {
user1: self.currentUser,
user2: self.otherUser
}
}).then(function (response) {
self.messages = response.data;
}).catch(function (error) {
console.log(error);
});
}
},
watch: {
otherUser: function (newValue) {
var self = this;
self.loadHistory();
}
}
});
```
注意事项:
1. Redis的键名使用了前缀“chat:”,以便区分其他数据;
2. 存储历史消息和订阅消息时,使用了当前用户的名称作为频道名称;
3. 在订阅消息时,使用了内部类MessageListener处理接收到的消息,然后发送给对应的WebSocketSession;
4. 在WebSocketSession关闭时,需要取消订阅Redis频道,以免造成资源浪费;
5. 前端使用了STOMP协议进行通信,需要安装sockjs-client和stompjs库;
6. 前端通过WebSocket连接到后端时,需要指定当前用户;
7. 前端通过WebSocket接收到消息时,需要将消息添加到消息列表中;
8. 前端通过REST API加载历史消息时,需要指定当前用户和对方用户。
这是一个基础的示例,具体实现可以根据自己的需求进行调整。
阅读全文