本文共 14182 字,大约阅读时间需要 47 分钟。
websocket与STMOP的比较及使用步骤
前言
介绍了websocket与STMOP以及SockJs使用代码,基本上拿来即可使用。也介绍了2者的区别及个人推荐使用websocket方式。
websocket在Html5里使用,主要目标还是解决服务器能主动向客户端发送消息的功能。也就是全双工的通信方式。 Spring提供了对Websocket的支持,WebSocket API是比较低级的API,但恰恰是我比较中意的方式。因为接近底层,能控制的逻辑比较多。源码是”webSocket+sockJs+STOMP 包括spring-websocket官方源码(
STOMP
方式)和自己实现的源码(webSocket
)”。 WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是底层协议,而 STOMP 是基于 WebSocket(SockJS) 的上层协议。
我个人更喜欢使用WebSocket 是底层协议。
,WebSocket将发送的对象相对比较集中,对代码的侵入比较少,而STOMP会有大量的@SendTo
侵入式编程。 WebSocket和Socket结合一起使用
下面的SockJs
和STMOP
引入同样的依赖即可
引入依赖
gradle
dependencies { compile("org.springframework.boot:spring-boot-starter-websocket") compile("org.webjars:stomp-websocket:2.3.3")}
maven
org.springframework.boot spring-boot-starter-websocket org.webjars stomp-websocket 2.3.3
java-config配置
拿来即可。
WebSockt
使用起来比较简单,只要配置handler
处理数据逻辑,Config
接收数据,拦截器处理前置或后置情况下逻辑即可。配置前端。 WebSocketConfig
.java package webSocketSockJs;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;/** * Description * * @author dyoon * @date 2018-06-29 */@Configuration@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //允许连接的域,只能以http或https开头,或者使用*号允许全部域名访问 String[] allowsOrigins = { "http://www.localhost.com"}; //WebSocket通道, //addHandler:注册端点/webSocketIMServer处理程序handler,是收集和消息分发中心。 //setAllowedOrigins:限制域名,如果不限制则改成setAllowedOrigins(*) //addInterceptors:增加拦截器 //一般很少用,一般使用withSockJS(),使接口支持SocketJS功能 registry.addHandler(myHandler(),"/webSocketIMServer").setAllowedOrigins(allowsOrigins).addInterceptors(webSocketInterceptor()); //SockJs通道,配置逻辑同上,只是多了withSockJS()方法,使其支持SocketJS功能 registry.addHandler(myHandler(), "/sockjs/webSocketIMServer").setAllowedOrigins(allowsOrigins).addInterceptors(webSocketInterceptor()).withSockJS(); } @Bean public MyHandler myHandler() { return new MyHandler(); } @Bean public WebSocketInterceptor webSocketInterceptor(){ return new WebSocketInterceptor(); }}
注册配置表,其实不难,拿来即用。有一个端点路径,是客户端连接地址。相当于统一入口,在接收或分发消息时,使用拦截器增加了前置和后置处理程序,同时真正的业务逻辑交给handler处理。
处理器handler
处理器ChatWebSocketHandler
.java,继承TextWebSocketHandler类,使TextMessage,BinaryMessage,PongMessage
package webSocketSockJs;import org.springframework.stereotype.Service;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;import java.util.HashMap;import java.util.Map;import java.util.Set;public class MyHandler extends TextWebSocketHandler { //在线用户列表 private static final Mapusers; //用户标识 private static final String CLIENT_ID = "clientId"; static { users = new HashMap<>(); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("成功建立连接"); Integer userId = getClientId(session); System.out.println(userId); if (userId != null) { users.put(userId, session); session.sendMessage(new TextMessage("成功建立socket连接")); System.out.println(userId); System.out.println(session); } } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { System.out.println(message.getPayload()); WebSocketMessage message1 = new TextMessage("hi "+message.getPayload()); try { session.sendMessage(message1); } catch (IOException e) { e.printStackTrace(); } } /** * 发送信息给指定用户 * @param clientId * @param message * @return */ public boolean sendMessageToUser(Integer clientId, TextMessage message) { if (users.get(clientId) == null) return false; WebSocketSession session = users.get(clientId); System.out.println("sendMessage:" + session); if (!session.isOpen()) return false; try { session.sendMessage(message); } catch (IOException e) { e.printStackTrace(); return false; } return true; } /** * 广播信息 * @param message * @return */ public boolean sendMessageToAllUsers(TextMessage message) { boolean allSendSuccess = true; Set clientIds = users.keySet(); WebSocketSession session = null; for (Integer clientId : clientIds) { try { session = users.get(clientId); if (session.isOpen()) { session.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); allSendSuccess = false; } } return allSendSuccess; } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (session.isOpen()) { session.close(); } System.out.println("连接出错"); users.remove(getClientId(session)); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { System.out.println("连接已关闭:" + status); users.remove(getClientId(session)); } @Override public boolean supportsPartialMessages() { return false; } /** * 获取用户标识 * @param session * @return */ private Integer getClientId(WebSocketSession session) { try { Integer clientId = Integer.valueOf(String.valueOf(session.getAttributes().get(CLIENT_ID))); return clientId; } catch (Exception e) { return null; } }}
对每个函数功能不作解释,有二点要注意
- 我们也可以通过H5在
new WebSocket(url)
中,在url传入标识参数,比如ws:hocalhost:8080/webSocketIMServer?userId=用户id值
。在handler里拿到数据。 - 在
TextWebSocketHandler
类里有handleMessage
方法,如下
@Override public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception { if (message instanceof TextMessage) { handleTextMessage(session, (TextMessage) message); } else if (message instanceof BinaryMessage) { handleBinaryMessage(session, (BinaryMessage) message); } else if (message instanceof PongMessage) { handlePongMessage(session, (PongMessage) message); } else { throw new IllegalStateException("Unexpected WebSocket message type: " + message); } }
可以处理3种类型数据。支持TextMessage,BinaryMessage,PongMessage
。实现不同方法即可。
拦截器Interceptor
WebSocketInterceptor.java
package webSocketSockJs;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.HandshakeInterceptor;import javax.servlet.http.HttpSession;import java.util.Map;public class WebSocketInterceptor implements HandshakeInterceptor { //handler处理前调用,attributes属性最终在WebSocketSession里,可能通过webSocketSession.getAttributes().get(key值)获得 @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Mapattributes) throws Exception { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; Object clientId = serverHttpRequest.getServletRequest().getParameter("clientId"); System.out.println(clientId); attributes.put("clientId", clientId); } return true; } //handler处理后调用 @Override public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { }}
attributes属性最终在WebSocketSession里,可能通过webSocketSession.getAttributes().get(key值)获得。
SockJs是什么
一些浏览器中缺少对WebSocket
的支持,SocketJS
是一种备选解决方案。SockJS
优先使用原生WebSocket
,如果在不支持websocket
的浏览器中,会自动降级为轮询的方式。 它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。
“http://”或“https://”
模式,而不是“ws://”和“wss://”
服务器通过withSockJS()
方法来使用SockJS作为备用方法 客户端实现,SockJS与WebSocket一起使用
前端引入SockJS模块。<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
//建立连接function connect() { var host = window.location.host; if ('WebSocket' in window) { websocket = new WebSocket("ws://" + host + "/webSocketIMServer?clientId=6"); } else if ('MozWebSocket' in window) { websocket = new MozWebSocket("ws://" + host + "/webSocketIMServer?clientId=6"); } else { websocket = new SockJS("http://" + host + "/sockjs/webSocketIMServer?clientId=6"); } websocket.onopen = function(evnt) { console.log("websocket连接上"); setConnected(true); }; websocket.onmessage = function(evnt) { console.log("接收到的消息:" + evnt.data); showGreeting(evnt.data); }; websocket.onerror = function(evnt) { console.log("websocket错误"); }; websocket.onclose = function(evnt) { console.log("websocket关闭"); }}//向服务器发送消息function sendName() { websocket.send(JSON.stringify({ 'name': $("#name").val()}));
上面判断浏览器支持WebSocket
则使用原生WebSocket
,否则使用SockJS
,SockJS所处理的URL是“http://”或“https://”模式,而不是“ws://”和“wss://”。上面客户端代码拿来即用。
总结客户端与服务端交互方式
建议连接后
- 客户端向服务端发送消息:websocket.send(JSON.stringify({'name': $("#name").val()}));
,数据最终进入handler里的handleTextMessage(WebSocketSession session, TextMessage message)
方法里。 - 服务端向客户端发送消息。 任意类里使用 “` @Autowired MyHandler handler; //调用方法,发送消息boolean hasSend = handler.sendMessageToUser(6, new TextMessage("发送一条小xi"));```
STMOP协议
spring-web-socket官网上的源码为例
. 官网使用的是STMOP协议。WebSocketConfig
类,继承WebSocketMessageBrokerConfigurer
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic6"); config.setApplicationDestinationPrefixes("/app"); // 点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/ // registry.setUserDestinationPrefix("/user/"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").setAllowedOrigins("*").withSockJS(); }}
@Configuration
注解用于定义配置类,使用java-config方式配置。@EnableWebSocketMessageBroker
用于开启STMOP
。
WebSocketMessageBrokerConfigurer
类即可,也可以继承AbstractWebSocketMessageBrokerConfigurer
,它是WebSocketMessageBrokerConfigurer
接口的空实现类。 enableSimpleBroker
这个方法用于配置主题,内容可以任意写,支持可变参数。可以把它理解为不同的业务定义不同的主题,每个主题下有多个子主题。这里的/topic6是随意写的。 setApplicationDestinationPrefixes
方法定义了请求的前缀是/app
,可被controller
拦截,能进入controller层,否则不能进入controller层。 addEndpoint
定义了端点,可以理解为客户端连接地址,连接成功即可使用webSocket的API,setAllowedOrigins
(“*”)定义了可以跨域,可以限制域名,比如 .setAllowedOrigins({"http://www.localhost.com"})
, withSockJS()
方法定义了支持SockJS
连接,优先使用原生的WebSockt
,如果浏览器不支持,则降级使用SockJs
。 Greeting.java和HelloMessage.java是2个纯java类,没啥介绍的。
控制层
GreetingController.java
package hello;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.annotation.SubscribeMapping;import org.springframework.stereotype.Controller;import org.springframework.web.util.HtmlUtils;@Controllerpublic class GreetingController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @MessageMapping("/hello") @SendTo("/topic6/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } @SubscribeMapping("/topic6/greetings") @SendTo("/topic6/greetings") public Greeting sub() { logger.info("XXX用户订阅了我。。。"); return new Greeting("感谢你订阅了我。。。"); }}
@MessageMapping
是接收客户端发送的消息映射,看名字也知道。由于上面定义了controll层拦截请求的前缀,所以请求/app/hello
才能进入controller层。
@SendTo("/topic6/greetings")
定义了向哪个主题发送消息。如果不定义,/app/hello
不会向任意主题发送消息,同时该接口也不会返回任意数据。/app/hello
接口的目的是接收消息,同时广播或定向发送给其它用户。 我添加了@SubscribeMapping
注解的方法,它是拦截订阅请求的,其实它的作用并不大,它也是必需接收/app/topic6/greetings
形势接口(注意有/app前缀),如果该注解下面没有@SendTo
,则会向订阅了/app/topic6/greetings
主题的用户发送消息。如果指定了@SendTo
,则使用它。 网上一图很方便于理解 这些配置已经完整的构成的使用STMOP协议的服务器了,只需要修改主题名,controller即可。 下面的重点是客户端。 //建议连接function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic6/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); });}发送消息function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({ 'name': $("#name").val()}));}
当客户端与服务端连接成功后,可以调用send()来发送STOMP消息。这个方法必须有一个参数,用来描述对应的STOMP的目的地。另外可以有两个可选的参数:headers,object类型包含额外的信息头部;body,一个String类型的参数。
总结客户端与服务端交互方式
建立连接后
- 客户端向服务端发送消息:
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
,进入controller层,接收消息。 服务端向客户端发送消息
- 一种情况,有客户端向服务端发送消息,可以使用
@SendTo
跳转 - 另一种情况,服务端主动发送消息 任意类中都可以
public class 任意类{ @Autowired private SimpMessagingTemplate messagingTemplate; //客户端只要订阅了/topic/subscribeTest主题,调用这个方法即可 public void templateTest() { messagingTemplate.convertAndSend("/topic/subscribeTest", new ServerMessage("服务器主动推的数据")); }}
- 一种情况,有客户端向服务端发送消息,可以使用
STMOP参考
客户端js使用请参数
转载地址:https://chenyuan.blog.csdn.net/article/details/80851512 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!