本文只做代码编程和部分讲解,如需了解一些概念问题请至其它博客进行了解
这边附上一个tomcat的实现连接tomcat+websocket
首先进行服务端编程
服务端代码已提交至github 服务端代码
本内容采用的是Spring boot,未实现权限控制,也没有实现用户登陆的功能
首先引入依赖
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
然后需要实现Principal接口进行重写其中的getName方法,给每一个连接的客户端唯一的身份标识
import java.security.Principal;
public class StompPrincipal implements Principal {
private String name;
public StompPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
还需要DefaultHandshakeHandler这个的配合,再编写一个继承DefaultHandshakeHandler的类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.security.Principal;
import java.util.Map;
@Component
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
private Logger log = LoggerFactory.getLogger(PrincipalHandshakeHandler.class);
private static int index = 0;
// Custom class for storing principal
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
//User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// Generate principal with UUID as name
log.info("开始构建----StompPrincipal----");
return new StompPrincipal(++index + "");
}
}
这边因为只集成了websocket,所以使用了一个静态变量index进行身份的唯一标识,在要使用的时候建议可以在构建StompPrincipal时给予登陆用户的唯一标识进行构建。在后边给指定用户发送消息时消息发送者的设置可以采用类似的方式。
需要一个所有已建立连接的客户端的管理类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.WebSocketSession;
import java.util.concurrent.ConcurrentHashMap;
public class SocketManager {
private static Logger log = LoggerFactory.getLogger(SocketManager.class);
private static ConcurrentHashMap<String, WebSocketSession> manager = new ConcurrentHashMap<String, WebSocketSession>();
public static void add(String key, WebSocketSession webSocketSession) {
log.info("新添加webSocket连接 {} ", key);
manager.put(key, webSocketSession);
}
public static void remove(String key) {
log.info("移除webSocket连接 {} ", key);
manager.remove(key);
}
public static WebSocketSession get(String key) {
log.info("获取webSocket连接 {}", key);
return manager.get(key);
}
}
在新客户端加入和断开连接时的通知类
import java.security.Principal;
@Component
public class WebSocketDecoratorFactory implements WebSocketHandlerDecoratorFactory {
private static Logger log = LoggerFactory.getLogger(WebSocketDecoratorFactory.class);
@Override
public WebSocketHandler decorate(WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("有人连接啦 sessionId = {}" , session.getId());
Principal principal = session.getPrincipal();
if (principal != null) {
log.info("key = {} 存入" , principal.getName());
SocketManager.add(principal.getName(), session);
// 身份校验成功,缓存socket连接
}
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("有人退出连接啦 sessionId = {}" , session.getId());
Principal principal = session.getPrincipal();
if (principal != null) {
// 身份校验成功,移除socket连接
SocketManager.remove(principal.getName());
}
super.afterConnectionClosed(session, closeStatus);
}
};
}
}
紧接着就是重点,websocket的配置类,许雅继承自AbstractWebSocketMessageBrokerConfigurer
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@CrossOrigin
@Configuration
@EnableWebSocketMessageBroker
//通过EnableWebSocketMessageBroker 开启使用STOMP协议来传输基于代理(message broker)的消息,此时浏览器支持使用@MessageMapping 就像支持@RequestMapping一样。
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketDecoratorFactory webSocketDecoratorFactory;
@Autowired
private PrincipalHandshakeHandler principalHandshakeHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// /ws/ep就是建立连接时连接的地址 http://ip:prit/ws/ep 可以自己定义
registry.addEndpoint("/ws/ep")
.setAllowedOrigins("*")
.setHandshakeHandler(principalHandshakeHandler)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/** queue用于注册点对点 topic用于组
* 其中注册queue时客户端需要添加前缀user
* 即:客户端 /user/queue/test
* 服务端:/queue/test
**/
registry.enableSimpleBroker(new String[]{"/queue","/topic"});
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(webSocketDecoratorFactory);
super.configureWebSocketTransport(registration);
}
}
再附上测试的类,首先一个消息类,根据需要自建
import java.util.Date;
public class ChatMsg {
private String from;
private String to;
private String content;
//消息存放
private Date date;
private String fromNickname;
private int type;//
@Override
public String toString() {
return "ChatMsg{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", content='" + content + '\'' +
", date=" + date +
", fromNickname='" + fromNickname + '\'' +
", type=" + type +
'}';
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getFromNickname() {
return fromNickname;
}
public void setFromNickname(String fromNickname) {
this.fromNickname = fromNickname;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
测试的controller
import cn.ffcs.websocket.model.ChatMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class WsController {
private static Logger log = LoggerFactory.getLogger(WsController.class);
@Autowired
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/ws/chat")
public void handleChat(ChatMsg chatMsg) {
chatMsg.setDate(new Date());
log.info("发送消息:" + chatMsg);
messagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
@MessageMapping("/ws/test")
public void handtest(ChatMsg chatMsg) {
chatMsg.setDate(new Date());
log.info("发送消息:" + chatMsg);
messagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/test", chatMsg);
}
@RequestMapping("/Welcome1")
public String say2()throws Exception{
messagingTemplate.convertAndSend("/topic/testgroup","hello");
return "is ok";
}
@MessageMapping("/testgroup")//浏览器发送请求通过@messageMapping 映射/welcome 这个地址。
@SendTo("/topic/testgroup")//服务器端有消息时,会订阅@SendTo 中的路径的浏览器发送消息。
public ChatMsg testgroup(ChatMsg message) throws Exception {
log.info(message.toString());
messagingTemplate.convertAndSend("/topic/testgroup",message);
return message;
}
@MessageMapping("/group/{id}")//浏览器发送请求通过@messageMapping 映射/welcome 这个地址。
@SendTo("/topic/group/{id}")//服务器端有消息时,会订阅@SendTo 中的路径的浏览器发送消息。
public ChatMsg group(ChatMsg message) throws Exception {
Thread.sleep(1000);
log.info(message.toString());
return message;
}
}
还需要指定服务端端口
server:
port: 8022
其中@MessageMapping
用于客户端在已经建立连接后发送请求的地址
messagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg)
中的三个参数分别为:第一个参数:发送给谁?,第二个参数:发送地址,如上述,客户端应建立连接为,需要增加/user前缀
stomp.subscribe("/user/queue/chat", message=> { }); }, failedMsg=> { });
第三个参数:发送的消息
messagingTemplate.convertAndSend("/topic/testgroup",message);
topic运用于向组发送消息,客户端不用增加user前缀,
后端代码的结构:
前端实现:
前端采用的vuetify框架,可以参考其实现,首先是依赖
package.json配置文件中dependencies 新增
"sockjs-client": "^1.5.0",
"stompjs": "^2.3.3",
然后运行
npm install
还需要配置一下跨域问题
vue.config.js
module.exports = {
transpileDependencies: [
'vuetify'
],
devServer: {
port: 8080,
proxy: {
'/ws/': {
target: 'http://127.0.0.11:8022/',
ws: true,
changeOrigin: true
},
'/api': {
target: 'http://127.0.0.11:8022/',
changeOrigin: true,
pathRewrite: {
'^/api': '' // rewrite path
}
}
}
}
}
连接服务端的方法我放在store中了,这个不了解的可以去其它博客去了解一下。
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
connect_chat_stomp:"",
connect_test_stomp:"",
connect_testgroup_stomp:"",
connect_groupId_stomp:""
},
mutations: {
},
actions: {
/* 通过chat连接接收消息*/
connect_chat(context){
context.state.connect_chat_stomp = Stomp.over(new SockJS("http://localhost:8022/ws/ep"));
console.log("连接ws/ep");
context.state.connect_chat_stomp.connect({}, frame=> {
console.log("{ws/chat}连接成功! 准备进行接收指定人员发送消息");
context.state.connect_chat_stomp.subscribe("/user/queue/chat", message=> {
console.log("{ws/chat}开始接收指定人员发送的消息内容");
console.log(message.body);
});
}, failedMsg=> {
console.log("{chat/test}接收指定消息的连接断开了");
});
},
/* 通过test连接接收消息 */
/* 注册点对点时需要在前边指定user:
* ontext.state.connect_test_stomp.subscribe("/user/queue/test", message=> ...】
*
* 消息时不需要user,如下消息将会发送到queue/test指定的人员:
* messagingTemplate.convertAndSendToUser(to, "/queue/test", chatMsg);】
*/
connect_test(context){
context.state.connect_test_stomp = Stomp.over(new SockJS("http://localhost:8022/ws/ep"));
console.log("连接ws/ep");
context.state.connect_test_stomp.connect({}, frame=> {
console.log("{ws/test}连接成功! 准备进行接收指定人员发送消息");
context.state.connect_test_stomp.subscribe("/user/queue/test", message=> {
console.log("{ws/test}开始接收指定人员发送的消息内容");
console.log(message.body);
});
}, failedMsg=> {
console.log("接收指定消息的连接断开了");
});
},
/* 连接testgroup组 */
connect_testgroup(context){
context.state.connect_testgroup_stomp = Stomp.over(new SockJS("http://localhost:8022/ws/ep"));
console.log("连接ws/ep");
context.state.connect_testgroup_stomp.connect({}, frame=> {
console.log("{/topic/testgroup}连接成功! 准备进行接收组内消息");
context.state.connect_testgroup_stomp.subscribe("/topic/testgroup", message=> {
console.log("{/topic/testgroup}开始接收组内消息");
console.log(message.body);
});
}, failedMsg=> {
console.log("接收指定消息的连接断开了");
});
},
/* 连接group指定id组 */
connect_groupId(context,id){
context.state.connect_groupId_stomp = Stomp.over(new SockJS("http://localhost:8022/ws/ep"));
console.log("连接ws/ep");
context.state.connect_groupId_stomp.connect({}, frame=> {
console.log("{topic/group/{id}}连接成功! 准备进行接收组内消息");
context.state.connect_groupId_stomp.subscribe("/topic/group/"+id, message=> {
console.log("{topic/group/{id}}开始接收组内消息");
console.log(message.body);
});
}, failedMsg=> {
console.log("接收指定消息的连接断开了");
});
},
}
})
紧接着发送消息的方法
api.sj 其中封装了axios的get请求方法
import axios from 'axios'
import router from '../router'
// import 'material-design-icons-iconfont/dist/material-design-icons.css'
let base = '/api/';
// let base = '';
export const getRequest = (url, params) => {
return axios({
method: 'get',
url: `${base}${url}`,
data: params
})
}
test.js
import store from '../store.js'
import {
getRequest
} from "./api";
/* 通过请求向/topic/testgroup{组}发送消息 */
export const sendTopic = () => {
getRequest("Welcome1").then(data => {
console.log(data);
})
}
/* 通过websocket向testgroup组发送消息 */
/* 对应
@MessageMapping("/testgroup")
@SendTo("/topic/testgroup")
*/
export const sendGroupByStamp = (msg) => {
if(store.state.connect_testgroup_stomp == ""){
alert("您还未建立连接!");
return;
}
store.state.connect_testgroup_stomp.send('/testgroup', {}, msg);
}
/* 通过websocket向group指定id组发送消息 */
/* 通过websocket向chat指定用户发送消息 */
/* 对应
@MessageMapping("/group/{id}")
@SendTo("/topic/group/{id}")*/
export const sendGroupIdByStamp = (msg, id) =>{
if(store.state.connect_groupId_stomp == ""){
alert("您还未建立连接!");
return;
}
store.state.connect_groupId_stomp.send('/group/' + id, {}, msg);
}
/* 通过websocket向test指定用户发送消息 */
/* 对应 @MessageMapping("/ws/test") */
export const sendTestByStamp = (msg) =>{
if(store.state.connect_test_stomp == ""){
alert("您还未建立连接!");
return;
}
store.state.connect_test_stomp.send('/ws/test', {}, msg);
}
/* 对应 @MessageMapping("/ws/chat") */
export const sendChatByStamp = (msg) =>{
if(store.state.connect_chat_stomp == ""){
alert("您还未建立连接!");
return;
}
store.state.connect_chat_stomp.send('/ws/chat', {}, msg);
}
测试页面
test.vue
<template>
<v-container class="grey lighten-5">
<v-card class="mx-auto" height="650px" max-width="450">
<v-row no-gutters>
<v-col cols="12" sm="6" offset-sm="2">
<v-text-field v-model="to" label="接收人"></v-text-field>
</v-col>
<v-col cols="12" sm="6" offset-sm="2">
<v-text-field v-model="context" label="发送内容"></v-text-field>
</v-col>
<v-col cols="12" sm="6" offset-sm="2">
<v-text-field v-model="groupId" label="分组id"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="connect_chat">连接/ws/chat</v-btn>
</v-col>
<v-col cols="2" offset-sm="2">
<v-btn @click="connect_test">连接/ws/test</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="connect_testgroup">连接/topic/testgroup</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="connect_groupId">连接topic/group/{id}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="tsendChatByStamp">发送至/ws/chat</v-btn>
</v-col>
<v-col cols="2" offset-sm="2">
<v-btn @click="tsendTestByStamp">发送至/ws/test</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="tsendGroupByStamp">websocket发送至/topic/testgroup</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="tsendTopic">request请求发送至/topic/testgroup</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="2" offset-sm="2">
<v-btn @click="tsendGroupIdByStamp">发送至topic/group/{id}</v-btn>
</v-col>
</v-row>
</v-card>
</v-container>
</template>
<script>
import {sendTopic} from '../../util/test.js'
import {sendGroupByStamp} from '../../util/test.js'
import {sendGroupIdByStamp} from '../../util/test.js'
import {sendTestByStamp} from '../../util/test.js'
import {sendChatByStamp} from '../../util/test.js'
export default {
name: 'App',
data() {
return {
to:"",
context:"",
groupId:""
};
},
methods:{
connect_chat(){
this.$store.dispatch('connect_chat');
},
connect_test(){
this.$store.dispatch('connect_test');
},
connect_testgroup(){
this.$store.dispatch('connect_testgroup');
},
connect_groupId(){
this.$store.dispatch('connect_groupId',this.groupId);
},
tsendTopic(){
sendTopic();
},
tsendGroupByStamp(){
let msgObj = new Object();
msgObj.to = this.to;
msgObj.content = this.context;
console.log("发送数据:" + JSON.stringify(msgObj));
sendGroupByStamp(JSON.stringify(msgObj));
},
tsendGroupIdByStamp(){
let msgObj = new Object();
msgObj.to = this.to;
msgObj.content = this.context;
console.log("发送数据:" + JSON.stringify(msgObj));
sendGroupIdByStamp(JSON.stringify(msgObj), this.groupId);
},
tsendTestByStamp(){
let msgObj = new Object();
msgObj.to = this.to;
msgObj.content = this.context;
console.log("发送数据:" + JSON.stringify(msgObj));
sendTestByStamp(JSON.stringify(msgObj));
},
tsendChatByStamp(){
let msgObj = new Object();
msgObj.to = this.to;
msgObj.content = this.context;
console.log("发送数据:" + JSON.stringify(msgObj));
sendChatByStamp(JSON.stringify(msgObj));
// this.$store.state.connect_chat_stomp.send('/ws/chat', {}, JSON.stringify(msgObj));
}
}
}
</script>
<style>
</style>
最后附上运行结果
连接服务端
没有连接的情况下发送数据
连接后向1号连接发送消息
连接指定的分组发送消息
更多的大家可以自己搭建测试然后根据自己需求进行实现…
后续将会出RabbmitMQ的入门讲解及与此合并的简介。
前端代码没有没有提交至svn,重点都在这篇里边写了。