我们熟悉的Http协议是一种无状态、无连接、单向的应用层协议,它采用了请求/响应模型。通信请求只能由客户端(浏览器)发起,服务端对请求做出响应处理,Http协议无法实现服务器向客户端发送消息(在服务器端发送变化的时候 比如发送公告)。在这种情况下websocket就应运而生。
Http这种单向请求,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数的web应用程序都是通过频繁的的异步javaScript和ajax请求进行长轮询。效率低下,非常的浪费资源。
webSocket链接允许客户端和服务端进行全双工通信,以便任意一方都可以建立连接将数据推送到另一端。webSocket只需要建立一次来链接,就可以一直保持链接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。



有一些浏览器中缺少对WebSocket的支持,而SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持WebSocket,会自动降为轮询的方式。
- <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();
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);
-
- });
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-websocketartifactId>
- 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
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());
- }
-
- }
- 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);
- }
-
STOMP 的消息根据前缀的不同分为三种。如下,以 /app 开头的消息都会被路由到带有@MessageMapping 或 @SubscribeMapping 注解的方法中;以/topic 或 /queue 开头的消息都会发送到STOMP代理中,根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制;以/user开头的消息会将消息重路由到某个用户独有的目的地上。

服务端处理客户端发来的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.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);
- }