原文链接
前言
网页端实时聊天,单纯使用HTTP
也不是不能做,但是确实相对来说不是一个理想的方案,这里我们使用spring-boot-starter-websocket
进行相关实现,更多功能可以查阅spring-websocket,以及Using WebSocket to build an interactive web application
客户端我们还是以Vue
作为示例,其他前端框架类似
服务端同本站其他文章一样,基于Java17
+Spring3
+Cloud4
,其他版本在部分代码中可能会稍有区别
实现
服务端
引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket
相关配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//客户端发送消息地址前缀
config.setApplicationDestinationPrefixes("/socket");
//服务端发送消息过滤前缀
config.enableSimpleBroker("/message", "/notify", "/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//建立连接地址
registry.addEndpoint("/chat-websocket")
.setAllowedOrigins("http://localhost:8080")
.withSockJS();
}
}
本地测试,这里我们需要增加.setAllowedOrigins("http://localhost:8080")
来解决跨域的问题
本质上就是由客户端首先通过三次握手向服务端建立连接,由addEndpoint
定义连接地址。然后可客户端发送消息,使用setApplicationDestinationPrefixes
设置地址消息地址前缀,功能类似于@RequestMapping::value
,使用enableSimpleBroker
过滤服务端发送消息的前缀,当然还有setUserDestinationPrefix
等其他配置,小伙伴们自行参考源码说明选用
以常用的两种实现举例,首先是广播通知指定团队的用户。当用户刚登录时,正常HTTP
请求服务端,获取未读的通知,这个走正常流程即可。当用户登录后,客户端通过订阅之前HTTP
请求得到的用户归属团队,订阅一个/多个地址,若后台需要发送广播时,调用服务端发送接口,定义发送的数个团队,则订阅该团队的用户即可收到消息,服务端简化代码如下:
@RestController
@RequestMapping("/user/message")
public class UserMessageController {
@Resource
public SimpMessageSendingOperations messagingTemplate;
@PostMapping("/notify")
public ResultObj<Object> notify(@RequestBody UserMessageNotifyParam param) {
for (String teamId : param.getTeamId()) {
messagingTemplate.convertAndSend(String.format("/notify/%s/receive", teamId), param.getMessage());
}
//todo save db
return ResultObj.success();
}
}
此时注意,message
的对象内容需要携带messageId
作为唯一键,用于解决用户订阅多个归属团队时,向其中多个发送同一条消息导致消息重复获取的问题
另一个常用的实现就是用户之间的消息发送,首先用户刚登陆时,和上例一样单纯走HTTP
获取未读消息,当用户登录后,通过自己的Id
订阅发送给自己的消息。作为另一个用户,给其发送消息时,有两种解决方案,一种是通过HTTP
发送,这种发送和上例差不多,只不过是需要修改发送地址以及改为和用户相关而不是team
相关,再加上相关的验证和鉴权操作,这里不再赘述,另一种解决方案是通过已经和服务端建立的TCP
通道发送消息,简化代码如下:
@RestController
@RequestMapping("/user/message")
public class UserMessageController {
@Resource
public SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/message/send")
public void userMsg(
@Payload String msg,
@Header("simpSessionId") String sessionId,
@Header("token") String token
) {
System.out.printf("[op:userMsg] Message is %s%n", msg);
System.out.printf("[op:userMsg] Session id is %s%n", sessionId);
System.out.printf("[op:userMsg] Token is %s%n", token);
//check auth
String sendUserId = tokenToUserId(token);
System.out.printf("[op:userMsg] User id is %s%n", sendUserId);
//get data
JSONObject msgObj = JSON.parseObject(msg);
String receiveUserId = msgObj.getString("userId");
String sendMsg = msgObj.getString("sendMsg");
//send
messagingTemplate.convertAndSendToUser(receiveUserId, "/message/receive", sendMsg);
//todo save db
}
}
至此,实时聊天的基本服务端就构建完成了,我们这里只提供了简化的代码,小伙伴在使用在生产时,肯定需要对其安全性和鲁棒性进行增强,比如网关过滤,可以参考websocket-spring-gateway-demo,以及使用消息队列减轻服务器压力等等
客户端
我们这里使用sockjs-client
加webstomp-client
的包进行socket
连接配置
{
//..
"dependencies": {
"sockjs-client": "^1.6.1",
"webstomp-client": "^1.2.6"
//..
}
//..
}
基本示例代码如下
<template>
<div>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">WebSocket connection:</label>
<button id="connect" class="btn btn-default" type="submit" :disabled="connected === true"
@click.prevent="connect">Connect
</button>
<button id="disconnect" class="btn btn-default" type="submit" :disabled="connected === false"
@click.prevent="disconnect">Disconnect
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="name">What is your name?</label>
<input type="text" id="name" class="form-control" v-model="send_message" placeholder="Your name here...">
</div>
<button id="send" class="btn btn-default" type="submit" @click.prevent="send">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody>
<tr v-for="item in received_messages" :key="item">
<td>{{ item }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import SockJS from "sockjs-client";
import Stomp from "webstomp-client";
export default {
name: "adminTest",
data() {
return {
received_messages: [],
send_message: null,
connected: false
};
},
methods: {
send() {
console.log("Send message:" + this.send_message);
if (this.stompClient && this.stompClient.connected) {
const msg = {userId: "666", sendMsg: this.send_message};
this.stompClient.send("/socket/message/send", JSON.stringify(msg), {token: "user_token"});
}
},
connect() {
this.socket = new SockJS("http://localhost:9527/chat-websocket");
this.stompClient = Stomp.over(this.socket);
this.stompClient.connect(
{},
frame => {
this.connected = true;
console.log(frame);
this.stompClient.subscribe("/user/777/message/receive", tick => {
console.log(tick);
this.received_messages.push(tick.body);
});
this.stompClient.subscribe("/notify/123/receive", tick => {
console.log(tick);
this.received_messages.push(tick.body);
});
this.stompClient.subscribe("/notify/456/receive", tick => {
console.log(tick);
this.received_messages.push(tick.body);
});
this.stompClient.subscribe("/notify/111/receive", tick => {
console.log(tick);
this.received_messages.push(tick.body);
});
},
error => {
console.log(error);
this.connected = false;
}
);
},
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
}
this.connected = false;
},
tickleConnection() {
this.connected ? this.disconnect() : this.connect();
}
},
};
</script>
<style scoped>
</style>
以上代码,基于该用户为666
,其订阅了123
,456
,111
团队,以及在尝试向用户777
发送消息的简化示例