一、WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
二、STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
三、首先,我们先理解一下为什么需要STOMP。
1)常规的websocket连接和普通的TCP基本上没有什么差别的。
2)那我们如果像http一样加入一些响应和请求层。
3)所以STOMP在websocket上提供了一中基于帧线路格式(frame-based wire format)。
4)简单一点,就是在我们的websocket(TCP)上面加了一层协议,使双方遵循这种协议来发送消息。
四、STOMP
1)Frame
例如:
command:CONNECT
其他部分都是headers的一部分。
2)command类别
CONNECT
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT
3)客户端常用连接方式
a、ws
var url = "ws://localhost:8080/websocket";
var client = Stomp.client(url);
b、sockJs
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script>
// use SockJS implementation instead of the browser's native implementation
var ws = new SockJS(url);
var client = Stomp.over(ws);
[...]
</script>
说明:使用ws协议需要浏览器的支持,但是一些老版本的浏览器不一定支持。Stomp.over(ws)的凡是就是用来定义服务websocket的协议。
4)服务端的实现过程
a、服务端:/app,这里访问服务端,前缀通过设定的方式访问。
b、用户:/user,这里针对的是用户消息的传递,针对于当前用户进行传递。
c、其他消息:/topic、/queue,这两种方式。都是定义出来用于订阅。并且消息只能从这里通过并处理
五、springboot的简单例子
1)目录结构
2)依赖包(pom.xml)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
3)websocket配置(WebSocketConfiguration、SecurityConfiguration)
/**
* webSocket配置
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
/**
* 注册stomp端点,主要是起到连接作用
* @param stompEndpointRegistry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry
.addEndpoint("/webSocket") //端点名称
//.setHandshakeHandler() 握手处理,主要是连接的时候认证获取其他数据验证等
//.addInterceptors() 拦截处理,和http拦截类似
.setAllowedOrigins("*") //跨域
.withSockJS(); //使用sockJS
}
/**
* 注册相关服务
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//这里使用的是内存模式,生产环境可以使用rabbitmq或者其他mq。
//这里注册两个,主要是目的是将广播和队列分开。
//registry.enableStompBrokerRelay().setRelayHost().setRelayPort() 其他方式
registry.enableSimpleBroker("/topic", "/queue");
//客户端名称前缀
registry.setApplicationDestinationPrefixes("/app");
//用户名称前
registry.setUserDestinationPrefix("/user");
}
}
认证配置:
/**
* 配置基本登录
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
/**
* 加密方式
*/
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 所有请求过滤,包含webSocket
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
/**
* 加入两个用户测试不同用的接受情况
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder.encode("admin")).roles("ADMIN")
.and()
.withUser("user").password(passwordEncoder.encode("user")).roles("USER");
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
4)服务端
/**
* 消息接收处理
*/
@RestController
public class MessageResource {
//spring提供的推送方式
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 广播模式
* @param requestMsg
* @return
*/
@MessageMapping("/broadcast")
@SendTo("/topic/broadcast")
public String broadcast(RequestMsg requestMsg) {
//这里是有return,如果不写@SendTo默认和/topic/broadcast一样
return "server:" + requestMsg.getBody().toString();
}
/**
* 订阅模式,只是在订阅的时候触发,可以理解为:访问——>返回数据
* @param id
* @return
*/
@SubscribeMapping("/subscribe/{id}")
public String subscribe(@DestinationVariable Long id) {
return "success";
}
/**
* 用户模式
* @param requestMsg
* @param principal
*/
@MessageMapping("/one")
//@SendToUser("/queue/one") 如果存在return,可以使用这种方式
public void one(RequestMsg requestMsg, Principal principal) {
//这里使用的是spring的security的认证体系,所以直接使用Principal获取用户信息即可。
//注意为什么使用queue,主要目的是为了区分广播和队列的方式。实际采用topic,也没有关系。但是为了好理解
messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/one", requestMsg.getBody());
}
}
客户端(JavaScript):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webSocket</title>
<script src="js/jquery.js"></script>
<script src="js/sockjs.min.js"></script>
<script src="js/stomp.js"></script>
</head>
<body>
<div>
<button id="connect">连接</button>
<button id="disconnect" disabled="disabled">断开</button>
</div>
<div>
<h3>广播形式</h3>
<button id="broadcastButton">发送</button><input id="broadcastText" type="text">
<label>广播消息:</label><input id="broadcastMsg" type="text" disabled="disabled">
</div>
<div>
<h3>订阅形式</h3>
<label>订阅消息:</label><input id="subscribeMsg" type="text" disabled="disabled">
</div>
<div>
<h3>角色形式</h3>
<button id="userButton">发送</button><input id="userText" type="text">
<label>用户消息:</label><input id="userMsg" type="text" disabled="disabled">
</div>
<div>
<h3>无APP</h3>
<button id="appButton">发送</button><input id="appText" type="text">
<label>前端消息:</label><input id="appMsg" type="text" disabled="disabled">
</div>
</body>
<script>
var stomp = null;
$("#connect").click(function () {
var url = "http://localhost:8080/webSocket"
var socket = new SockJS(url);
stomp = Stomp.over(socket);
//连接
stomp.connect({}, function (frame) {
//订阅广播
stomp.subscribe("/topic/broadcast", function (res) {
$("#broadcastMsg").val(res.body);
});
//订阅,一般只有订阅的时候在返回
stomp.subscribe("/app/subscribe/1", function (res) {
$("#subscribeMsg").val(res.body);
});
//用户模式
stomp.subscribe("/user/queue/one", function (res) {
$("#userMsg").val(res.body);
});
//无APP
stomp.subscribe("/topic/app", function (res) {
$("#appMsg").val(res.body);
});
setConnect(true);
});
});
$("#disconnect").click(function () {
if (stomp != null) {
stomp.disconnect();
}
setConnect(false);
});
//设置按钮
function setConnect(connectStatus) {
$("#connect").attr("disabled", connectStatus);
$("#disconnect").attr("disabled", !connectStatus);
}
//发送广播消息
$("#broadcastButton").click(function () {
stomp.send("/app/broadcast", {}, JSON.stringify({"body":$("#broadcastText").val()}))
});
//发送用户消息
$("#userButton").click(function () {
stomp.send("/app/one", {}, JSON.stringify({"body":$("#userText").val()}))
});
//发送web消息
$("#appButton").click(function () {
stomp.send("/topic/app", {}, JSON.stringify({"body":$("#appText").val()}))
});
</script>
</html>
5)普通测试
角色测试:
六、相关资料
http://jmesnil.net/stomp-websocket/doc/
七、源码:https://github.com/lilin409546297/springboot-websocket
来源:oschina
链接:https://my.oschina.net/u/4358626/blog/3585614