概念:
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用轮询。轮询效率低,且很浪费资源。
而WebSocket协议最大的特点就是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器端推送技术的一种。
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
用法:
html5中提供了WebSocket对象,
// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:8080');
// Connection opened
socket.addEventListener('open', function (event) {
socket.send('Hello Server!');
});
// Listen for messages
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
mdn接口说明
websocket没有规范payload格式,可以是文本数据,也可以发送二进制数据,需要我们自己定义。而我们可以使用stomp协议去规范传输数据格式标准。
什么是payload
通常在传输数据时,为了使数据传输更可靠,要把原始数据分批传输,并且在每一批数据的头和尾都加上一定的辅助信息,比如数据量的大小、校验位等,这样就相当于给已经分批的原始数据加一些外套,这些外套起标示作用,使得原始数据不易丢失,一批数据加上“外套”就形成了传输通道的基本传输单元,叫做数据帧或数据包,而其中的原始数据就是payload
什么是stomp
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议 。它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。
更多stomp over Websocket细节 参考官方文档 || 翻译文档
概念:
SockJS是一个提供类似websocket的对象的浏览器JavaScript库。SockJS为您提供了一个连贯的、跨浏览器的Javascript API,它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。
在幕后,SockJS首先尝试使用本机WebSockets。如果失败,它可以使用各种特定于浏览器的传输协议,并通过类似websocket的抽象来呈现它们。
SockJS旨在为所有现代浏览器和不支持WebSocket协议的环境(例如,受限的企业代理)工作。
(我们可以在github上找到SockJS更详细的使用文档)
创建stomp客户端
在web浏览器中使用原生的WebSocket
var url = "ws://localhost:61614/stomp";
var client = Stomp.client(url);
使用sockejs包装的websocket
连接stomp服务端
client.connect(login, passcode, connectCallback);
client.connect(login, passcode, connectCallback, errorCallback);
client.connect(login, passcode, connectCallback, errorCallback, host);
client.connect(headers, connectCallback); // headers为map结构
client.connect(headers, connectCallback, errorCallback);
发送消息
client.send(destination, headers, body); //destination 为地址 如 '/queue/default'
订阅消息
var subscription = client.subscribe(destination,callback,headers);
取消订阅
subscription.unsubscribe();
事务
// start the transaction
var tx = client.begin();
// send the message in a transaction
client.send("/queue/test", {transaction: tx.id}, "message in a transaction");
// commit the transaction to effectively send the message
tx.commit();
//or tx.abort();
org.springframework.boot
spring-boot-starter-websocket
创建一个消息处理控制类
@Controller
public class GreetingController {
@Autowired
private UserService userService;
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
//服务端映射地址,还需要加上代理地址前缀
@MessageMapping("/hello")
//@SendTo("/topic/greetings")
public void greeting(User user) throws Exception{
TimeUnit.SECONDS.sleep(2);
//手动去发送数据到客户端
simpMessagingTemplate.convertAndSend("/topic/greetings","hello spring stomp");
}
}
配置spring的stomp消息服务,spring的websocket服务实现,实际上是一个简易版的消息队列
@Configuration
@EnableWebSocketMessageBroker
public class SpringWebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 定义消息代理,设置消息连接的各种规范请求信息
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//启动一个简单基于内存的消息代理 返回客户端消息前缀/topic
registry.enableSimpleBroker("/topic");
//发送个服务端目的地前缀 /app 开头的数据会被@MessageMapping拦截 进入方法体
registry.setApplicationDestinationPrefixes("/app");
//点对点用户
registry.setUserDestinationPrefix("/user");
}
/**
* 添加一个服务端点,来接收客户端的连接。
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一个/gs-guide-websocket 端点,客户端通过这个端点进行连接, withSockJS 开启对SockJS支持
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
}
对于地址“/app”,发送到这类目的地址的消息在到达broker之前, 会先路由到由应用写的某个方法. 相当于对进入broker的消息进行一次拦截@MessageMapping, 目的是针对消息做一些业务处理.
对于地址“/topic”,发送到这类目的地址的消息会直接跳转到broker,不会被应用拦截
Title
STOMP消息的body必须为字符串
前端作为客户端,发送或订阅某个目的地地址。
var stompClient = null;
/**
* 建立连接并订阅消息
* @returns {*}
*/
function connect(){
var subscription;
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({},function (message) {
if(message.body){
console.log("got message with body " + message.body);
}else {
console.log("get empty message");
}
//订阅一个目的地
//STOMP消息的body必须为字符串
subscription = stompClient.subscribe('/topic/greetings',function(msg){
console.log("接受到java后台发送的消息," + msg.body);
});
})
return subscription;
}
/**
* 中止订阅消息
*/
function unsubscription(subscription){
if(subscription){
subscription.unsubscribe();
}
}
/**
* 断开连接
*/
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
function sendName(){
//STOMP消息的body必须为字符串
stompClient.send('/app/hello',{},JSON.stringify({'username':$("#name").val()}));
//直接发送数据,不被拦截
//stompClient.send('/topic/greeting',{},JSON.stringify({'username':$("#name").val()}));
}
$(function(){
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
})
建立连接时的日志
Opening Web Socket...
stomp.js:134 Web Socket Opened...
stomp.js:134 >>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
<<< CONNECTED
version:1.1
heart-beat:0,0
connected to server undefined
get empty message
>>> SUBSCRIBE
id:sub-0
destination:/topic/greetings
发送消息时的日志。客户端向服务端发送的数据文本为{"username":"food"},服务端返回的数据文本为 hello spring stomp
>>> SEND
destination:/app/hello
content-length:19
{"username":"food"}
<<< MESSAGE
destination:/topic/greetings
content-type:text/plain;charset=UTF-8
subscription:sub-0
message-id:qz2tzbed-0
content-length:18
hello spring stomp
接受到java后台发送的消息,hello spring stomp
参考: spring 官网文档 using websocket
stomp over websocket 文档翻译
github sockjs