1、由来
我们熟悉的Http协议是一种无状态、无连接、单向的应用层协议,它采用了请求/响应模型。通信请求只能由客户端(浏览器)发起,服务端对请求做出响应处理,Http协议无法实现服务器向客户端发送消息(在服务器端发送变化的时候 比如发送公告)。在这种情况下websocket就应运而生。
Http这种单向请求,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数的web应用程序都是通过频繁的的异步javaScript和ajax请求进行长轮询。效率低下,非常的浪费资源。
webSocket链接允许客户端和服务端进行全双工通信,以便任意一方都可以建立连接将数据推送到另一端。webSocket只需要建立一次来链接,就可以一直保持链接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
?
2、样式
?
3、简介?
有一些浏览器中缺少对WebSocket的支持,而SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持WebSocket,会自动降为轮询的方式。
<script src="/js/appjs/oa/webSocket/sockjs.min.js"></script>
<script src="/js/appjs/oa/webSocket/stomp.min.js"></script>
发起连接
//创建连接对象 未连接
var sock = new SockJS("/endpointChat");
// 获取 STOMP 子协议客户端对象
var stomp = Stomp.over(sock);
//方法签名
stomp.connect(headers, connectCallback, errorCallback);
说明: 1)?socket连接对象也可通过WebSocket(不通过SockJS)连接
var socket=new WebSocket("/spring-websocket-portfolio/portfolio");
其中 headers表示客户端的认证信息,如:
var headers = {
login: 'mylogin',
passcode: 'mypasscode',
// additional header
'client-id': 'my-client-id'
};
若无需认证,直接使用空对象 “{}” 即可;
connectCallback 表示连接成功时(服务器响应 CONNECTED 帧)的回调方法; errorCallback 表示连接失败时(服务器响应 ERROR 帧)的回调方法,非必须;
断开连接
stomp.disconnect();
发送信息
连接成功后,客户端可使用 send() 方法向服务器发送信息
client.send( url, headers, body);
?其中 url 为服务器 controller中 @MessageMapping 中匹配的URL,字符串,必须参数; headers 为发送信息的header,JavaScript 对象,可选参数; body 为发送信息的 body,字符串,可选参数;
var payload = JSON.stringify({'message':username})
stomp.send("/app/welcome",{},payload);
@Controller
public class WebSocketController {
@MessageMapping("/welcome") // 浏览器发送请求通过@messageMapping 映射/welcome 这个地址。
//@SendTo("/topic/getResponse") // 服务器端有消息时,会订阅@SendTo 中的路径的浏览器发送消息。
@SendTo("/queue/notifications")
public Response say(Message message) throws Exception {
Thread.sleep(1000);
return new Response("Welcome, " + message.getMessage() + " !");
}
}
订阅、接收信息
STOMP 客户端要想接收来自服务器推送的消息,必须先订阅相应的URL,即发送一个 SUBSCRIBE 帧,然后才能不断接收来自服务器的推送消息; 订阅和接收消息通过 subscribe() 方法实现:
subscribe(destination url, callback, headers)
其中 destination url 为服务器 @SendTo 匹配的 URL,字符串; callback 为每次收到服务器推送的消息时的回调方法,该方法包含参数 message; headers 为附加的headers,JavaScript 对象;什么作用? 该方法返回一个包含了id属性的 JavaScript 对象,可作为 unsubscribe() 方法的参数;
例:
stomp.subscribe('/topic/getResponse', function (message) { //订阅/topic/getResponse 目标发送的消息。这个是在控制器的@SendTo中定义的。
if (message.body) {
alert("got message with body " + message.body)
} else {
alert("got empty message");
}
});
取消订阅?
var subscription = client.subscribe(...);
subscription.unsubscribe();
JSON 支持
STOMP 帧的 body 必须是 string 类型,若希望接收/发送 json 对象,可通过 JSON.stringify() and JSON.parse() 实现; 例:?
var quote = {symbol: 'APPL', value: 195.46};
client.send("/topic/stocks", {}, JSON.stringify(quote));
client.subcribe("/topic/stocks", function(message) {
var quote = JSON.parse(message.body);
alert(quote.symbol + " is at " + quote.value);
});
4、使用
导入依赖
<dependency>
? ?<groupId>org.springframework.boot</groupId>
? ?<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类
/**
* 通过EnableWebSocketMessageBroker 开启使用STOMP协议来传输基于代理(message broker)的消息,
* 此时浏览器支持使用@MessageMapping 就像支持@RequestMapping一样。
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 扫描@ServerEndpoint,将@ServerEndpoint修饰的类注册为websocket
* 如果使用外置tomcat,则不需要此配置
*/
@Bean
public ServerEndpointExporter serverEndpointExporter()
{
return new ServerEndpointExporter();
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) { //endPoint 注册协议节点,并映射指定的URl
//注册一个名字为"endpointChat" 的endpoint,并指定 SockJS协议。 点对点-用
registry.addEndpoint("/endpointChat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置消息代理前缀
// 即如果消息的前缀是 /topic ,就会将消息转发给消息代理(broker),
// 再由消息代理将消息广播给当前连接的客户端。
//点对点式增加一个/queue 消息代理
registry.enableSimpleBroker("/queue", "/topic");
//客户端向服务端发起请求时,需要以/app为前缀。
registry.setApplicationDestinationPrefixes("/app");
}
}
方式一 监控连接
后台自己实现Endpoint,前端使用内置的WebSocket。
@ServerEndpoint("/websocket")
@Component //放到spring容器中
@Slf4j
public class WebSocketServer{
/**
* 所有连接的客户端
*/
private static ConcurrentHashMap<String,Session> clients = new ConcurrentHashMap<>();
/**
* 建立连接时调用的方法
*/
@OnOpen
public void onOpen(Session session) {
clients.put(session.getId(),session);
//向特定用户发送消息,使用的session是接收方的session
session.getAsyncRemote().sendText("已加入群聊");
}
/**
* 连接关闭时调用的方法
*/
@OnClose
public void onClose(Session session) {
clients.remove(session.getId());
session.getAsyncRemote().sendText("已退出群聊");
}
/**
* 收到客户端发送过来的消息时调用的方法
* @param msg 客户端用户发送过来的消息,二进制可以声明为byte[]
*/
@OnMessage
public void onMessage(String msg) {
//群发消息
for (Session session : clients.values()) {
session.getAsyncRemote().sendText(msg);
}
}
/**
* 发生错误时调用的方法
*/
@OnError
public void onError(Session session, Throwable e) {
log.error("发送错误的sessionId:"+session.getId()+",错误信息:"+e.getMessage());
}
}
<script>
let socket;
//手动打开连接
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您使用的浏览器不支持WebSocket");
}else{
//连接到websocket的某个endpoint
socket = new WebSocket("ws://127.0.0.1:8080/websocket");
//以下几个方法相当于事件监听,在特定事件触发时会自动调用
socket.onopen = () => {
console.log("已连接到websocket");
};
socket.onmessage = resp => {
console.log("接收到服务端信息:" + resp.data);
};
socket.onclose = () => {
console.log("已断开websocket连接");
};
socket.onerror = () => {
console.log("websocket发生错误");
}
}
}
//手动关闭连接
function closeSocket() {
socket.close();
}
//发送消息到服务器
function sendMsg(msg) {
//参数不一定要是字符串类型,可以是任意类型(二进制数据)
socket.send(msg);
}
</script>
方式二?
1、启用STOMP功能
? ? STOMP 的消息根据前缀的不同分为三种。如下,以 /app 开头的消息都会被路由到带有@MessageMapping 或 @SubscribeMapping 注解的方法中;以/topic 或 /queue 开头的消息都会发送到STOMP代理中,根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制;以/user开头的消息会将消息重路由到某个用户独有的目的地上。
?
2、处理来自客户端的STOMP消息
? ? 服务端处理客户端发来的STOMP消息,主要用的是?@MessageMapping 注解。如下:
@MessageMapping("/welcome") // 浏览器发送请求通过@messageMapping 映射/welcome 这个地址。
@SendTo("/queue/notifications") // 服务器端有消息时,会订阅@SendTo 中的路径的浏览器发送消息。
public Response say(Message message) throws Exception {
Thread.sleep(1000);
return new Response("Welcome, " + message.getMessage() + " !");
}
?
?2.3、尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅。@SendTo 注解重写了消息代理的目的地,如果不指定@SendTo,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会添加上“/topic”前缀。
? ? 2.4、如果客户端就是想要服务端直接返回消息呢?听起来不就是HTTP做的事情!即使这样,STOMP 仍然为这种一次性的响应提供了支持,用的是@SubscribeMapping注解,与HTTP不同的是,这种请求-响应模式是异步的...
@SubscribeMapping("/getShout")
public Shout getShout(){
Shout shout = new Shout();
shout.setMessage("Hello STOMP");
return shout;
}
3、发送消息到客户端
3.1 在处理消息之后发送消息
? ? 正如前面看到的那样,使用?@MessageMapping 或者?@SubscribeMapping 注解可以处理客户端发送过来的消息,并选择方法是否有返回值。
? ? 如果 @MessageMapping 注解的控制器方法有返回值的话,返回值会被发送到消息代理,只不过会添加上"/topic"前缀。可以使用@SendTo 重写消息目的地;
? ? 如果 @SubscribeMapping 注解的控制器方法有返回值的话,返回值会直接发送到客户端,不经过代理。如果加上@SendTo 注解的话,则要经过消息代理。
3.2 在应用的任意地方发送消息
? ? spring-websocket 定义了一个?SimpMessageSendingOperations 接口(或者使用SimpMessagingTemplate ),可以实现自由的向任意目的地发送消息,并且订阅此目的地的所有用户都能收到消息。
@Autowired
private SimpMessagingTemplate template;
/**
* 广播消息,不指定用户,所有订阅此的用户都能收到消息
* @param shout
*/
@MessageMapping("/broadcastShout")
public void broadcast(Shout shout) {
template.convertAndSend("/topic/shouts", shout);
}
????除了convertAndSend()以外,SimpMessageSendingOperations 还提供了convertAndSendToUser()方法。按照名字就可以判断出来,convertAndSendToUser()方法能够让我们给特定用户发送消息。
@MessageMapping("/singleShout")
public void singleUser(Shout shout, StompHeaderAccessor stompHeaderAccessor) {
String message = shout.getMessage();
LOGGER.info("接收到消息:" + message);
Principal user = stompHeaderAccessor.getUser();
simpMessageSendingOperations.convertAndSendToUser(user.getName(), "/queue/shouts", shout);
}
|