14-WebSocket 背景对于实时响应、更新的应用,即服务器可以即时将数据更新变化反映到客户端,传统的请求-响应模式效率低下,在处理此类业务场景时,通常采用的方案有:
短轮询:浏览器每隔一段时间向服务器发送http请求,这种方法浪费带宽,效率低下; 长轮询(comet):服务器收到客户端请求后,不直接进行响应,先讲请求挂起,判断请求数据是否有更新,如果有,则响应;没有,则到达一定时间限制后,关闭连接。这种方法缺点在于,连续挂起同样导致资源浪费; SSE:全称Server-SentEvents,允许服务器主动推送数据到客户端,本质与长轮询、短轮询不同; WebSocket:HTML5定义的WebSocket协议,它是独立的,创建在TCP之上的协议,该协议可以实现服务器与客户端之间的全双工通信; 本文主要讲解WebSocket。
WebSocket介绍WebSocket是HTML5一种新的协议,建立在传输层TCP协议之上,实现了客户端和服务器的全双工异步通信。协议简写为ws
,wss
是加密版本。
和HTTP协议最大不同有:
WebSocket是双向通信协议,服务器和客户端都能主动向对方发送数据; WebSocket需要类似TCP的握手链接,连接成功后才能相互通信; WebSocket连接后,服务器和客户端就可以发送数据,直到连接关闭。
WebSocket建立连接WebSocket握手连接阶段,需要使用HTTP协议。
来看一个WeboSocket握手例子。
一个HTTP请求报文。
1 2 3 4 5 6 7 8 GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
与普通的HTTP请求相比,多了几样东西。
1 2 Connection: Upgrade Upgrade: websocket
Connection
表示想升级协议,Upgrade
表示想升级协议到WebSocket
。
Set-WebSocket-Version
表示websocket版本。
服务器端相应格式为:
1 2 3 4 HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
状态101表示协议切换成功。
STOMPSTOMP-Simple Text Oriented Mesage Protocol,面向消息的简单文本协议。
因为WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义;
与处在应用层HTTP不同,WebSocket处在TCP上很薄一层,仅仅将字节流转换成文本/二进制消息,因此对于实际应用来说,WebSocket通信层级过低。
所以可以在WebSocket之上使用STOMP协议,为浏览器和服务器之间通信增加适当的消息语义。
STOMP协议提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。
如何理解STOMP和WebSocket的关系:
HTTP协议解决了web浏览器发起请求及服务器响应的细节,直接使用WebSocket编程,就好像没有HTTP协议,直接用TCP套接字编写应用一样; STOMP是上层协议,WebSocket是底层协议,STOMP在WebSocket上,明确了信息交互双方,消息格式等; STOMP服务端STOMP服务端被设计为客户端可以向其发送消息的一组目标地址,STOMP协议没有规定目标地址格式,可以由应用定义,例如/topic/a
,topic/b
等。应用可以规定不同格式表明不同含义。
例如应用定义以/topic
打头的发布订阅模式,消息被所有消费者客户端收到;以/user
开头的为点对点模式,只会被一个消费者客户端收到。
STOMP客户端对于STOMP协议来说,客户端会扮演下列两种角色一种:
作为生产者,通过SEND帧发送消息到指定地址; 作为消费者,通过发布SUBSCRIBE帧到已知地址进行消息订阅,当生产者发送消息到这个订阅地址后,订阅该地址的其他消费者会通过MESSAGE帧收到该消息。 因此,WebSocket结合STOMP相当于构建了一个消息分发队列,客户端可以在上述两种角色间转换,订阅机制保证了一个客户端消息可以通过服务器广播到其他客户端,作为生产者,又可以通过服务器发送点对点消息。
SockJSSockJS是一个JavaScript库,是为了应对许多浏览器不支持WebSocket协议的问题。SockJS是WebSocket技术的一种模拟,会尽可能对应WebSocket API,优先选择WebSocket连接。但是当服务器或客户端不支持WebSocket时,会使用其他方案例如轮询、iFrame等中择优连接。
SpringBoot集成WebSocketSpringBoot集成WebSocket有多种方式,这里介绍三种方法:
首先导入所需库。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
原生注解使用jdk自带的原生注解实现。
ServerEndpoint
表示客户端用来连接服务端的地址,例如ServerEndpoint
为/websocket
,则客户端可使用ws://ip:port/websocket
进行websocket连接。
创建WebSocketConfig配置文件1 2 3 4 5 6 7 8 @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
这个配置类很简单,注入ServerEndpointExporter
,这个Bean
会自动注册使用了ServerEndpoint
注解声明的WebSocket Endpoint。
通过这个Bean
,SpringBoot才能扫描到关于websocket注解。
WebSocket处理类使用Component
注解表示这是由spring容器管理的对象。虽然Component
默认是单例模式,但SpringBoot还是会为每个WebSocket连接初始化一个Bean
,所以可以用静态Map保存起来。
换句话说,每次一个客户端向服务端发起WebSocket连接时,都会创建一个WebSocket对象,同时建立连接时,会自动创建一个Session
对象,也就是说,每个连接对应唯一一个Session
,服务端发送消息须通过Session
。
其他注解,都是在javax.websocket
下,并不是spring提供的,而是jdk自带的:
@ServerEndpoint
:通过这个注解,暴露ws连接路径,类似RequestMapping
注解,如果值为ws
,可以通过ws://127.0.0.1:80/ws
进行websocket连接;@OnOpen
:方法注解,当任意一个客户端建立websocket连接成功后会触发这个注解修饰的方法,注意有一个Session参数,这个Session是WebSocket的,还可以加入带@PathParam
注解的参数;@OnClose
:方法注解,当某个客户端断开websocket连接后会触发这个注解修饰的方法,注意有一个Session参数;@OnMessage
:方法注解,当客户端发送消息到服务端时,会触发这个注解修饰的方法,有一个String参数,表示客户端传入消息;@OnError
:当websocket建立连接出现异常时,会触发该方法服务端向客户端发送消息必须通过上面提到的Session
类,通常是在OnOpen
方法中,当连接成功后把Session
存入Map的value中,当需要发送时,通过key
获得session
发送,通过session.getBasicRemote().sendText()
向客户端发送消息。
下面例子使用ConCurrentHashMap
保存所有连接,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 @Slf4j @Component @ServerEndpoint("/websocket/{name}") public class WebSocket { private Session session; private String name; private static ConcurrentHashMap<String, WebSocket> webSockets = new ConcurrentHashMap <>(); @OnOpen public void OnOpen (Session session, @PathParam(value = "name") String name) { log.info("---------------------------" ); this .session = session; this .name = name; webSockets.put(name ,this ); log.info("WebSocket 连接成功, 当前连接人数为: {}" , webSockets.size()); log.info("---------------------------" ); groupSending(name + "来了" ); } @OnClose public void OnClose () { webSockets.remove(this .name); log.info("WebSocket 退出成功, 当前连接人数为: {}" , webSockets.size()); groupSending(name + "走了" ); } @OnMessage public void OnMessage (String messageStr) { log.info("WebSocket 收到消息: {}" , messageStr); if (messageStr.startsWith("group" )) { String message = messageStr.split(":" )[1 ]; groupSending(message); } else { String[] strs = messageStr.split(":" ); String name = strs[0 ].split("-" )[1 ]; String message = strs[1 ]; appointSending(name, message); } } public void groupSending (String message) { for (String name : webSockets.keySet()) { try { webSockets.get(name).session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } public void appointSending (String name, String message) { try { webSockets.get(name).session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } }
使用websocket
在线测试网站,输入url:ws://127.0.0.1:8080/demo/websocket/user1
,表示user1对应客户端建立WebSocket连接。
Spring封装 创建WebSocketHandler继承TextWebSocketHandler
类,重写其中的几个方法:
afterConnectionEstablished
:连接建立后调用,与OnOpen
功能相同;afterConnectionClosed
:连接关闭后调用,与OnClose
功能相同;handleTextMessage
:服务端收到文本消息时调用,与OnMessage
功能相同;完整代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 @Slf4j @Component public class WebSocketHandler extends TextWebSocketHandler { public static ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap <>(); @Override public void afterConnectionEstablished (WebSocketSession session) throws Exception { String key = getName(session); sessions.put(key, session); log.info(key + ": 成功建立连接" ); } @Override public void afterConnectionClosed (WebSocketSession session, CloseStatus status) throws Exception { String key = getName(session); sessions.remove(key); log.info(key + ": 成功关闭连接" ); } @Override protected void handleTextMessage (WebSocketSession session, TextMessage message) throws Exception { log.info("WebSocket 收到消息" ); String messageStr = message.getPayload(); if (messageStr.startsWith("group" )) { String msg = messageStr.split(":" )[1 ]; groupSending(msg); } else { String[] strs = messageStr.split(":" ); String name = strs[0 ].split("-" )[1 ]; String msg = strs[1 ]; appointSending(name, msg); } } public void groupSending (String message) { for (String name : sessions.keySet()) { try { sessions.get(name).sendMessage(new TextMessage (message)); } catch (Exception e) { e.printStackTrace(); } } } public void appointSending (String name, String message) { try { sessions.get(name).sendMessage(new TextMessage (message)); } catch (Exception e) { e.printStackTrace(); } } private String getName (WebSocketSession session) { URI uri = session.getUri(); if (Objects.isNull(uri) || StrUtil.isEmpty(uri.toString())) { return "" ; } return uri.getPath().substring(uri.getPath().lastIndexOf("/" ) + 1 ); } }
创建WebSocketConfig
实现WebSocketConfigurer
接口,该接口只有一个方法:
void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
用于注册WebSocket
的处理类,并映射到对应地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers (WebSocketHandlerRegistry registry) { registry.addHandler(handler(), "/websocket/*" ).setAllowedOrigins("*" ); } @Bean public WebSocketHandler handler () { return new WebSocketHandler (); } }
STOMP集成Spring消息代理的WebSocket架构图为:
图中各个组件介绍:
生产者型客户端(左上组件):通过SEND
命令发送消息到某个目的地址 消费者型客户端(左下组件):订阅某个目的地址,并接收此目的地址推送过来的消息 request channel
:一组用来接收生产者型客户端推送过来消息的线程池response channel
:一组用来推送消息给消费者型客户端的线程池borker
:消息队列管理者,也可以成为消息代理。有自己的地址,客户端可以向其发送订阅指令,它会记录哪些客户端订阅了这个目的地址应用目的地址(图中app
):发送到这类目的地址的消息,在到达broker
之前,会先路由到由应用写的某个方法,相当于对broker
的消息进行一次拦截,目的是针对消息做一些业务处理 非应用目的地址(图中/topic
,也是消息代理地址):发送到这类目的地址的消息会直接转到broker
,不会被应用拦截 SimpAnnotationMethod
:发送到应用目的地址的消息在到达borker
之前,先路由到的方法,由应用控制 消息从生产者发出到消费者消费的流转流程生产者通过发送一条SEND
命令消息到某个目的地址destination
服务端request channel
接受到该条命令,如果目的地址是应用目的地址,则转到对应业务方法(SimpAnnotationMethod
)处理,再转到borker
(SimpleBroker
);如果目的地址是非应用目的地址,则直接转到borker
borker
通过SEND
命令消息构建MESSAGE
命令消息,再通过response channel
推送MESSAGE
命令消息到所有订阅此目的地址的消费者 配置类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints (StompEndpointRegistry registry) { registry.addEndpoint("/ws" ).setAllowedOrigins("*" ).withSockJS(); } @Override public void configureMessageBroker (MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic" , "/user" ); registry.setApplicationDestinationPrefixes("/app" ); registry.setUserDestinationPrefix("/user" ); } }
通过实现WebSocketMessageBrokerConfigurer
接口加上注解@EnableWebSocketMessageBroker
进行STOMP的配置和注解扫描 其中覆盖registerStompEndpoints
方法设置暴露的STOMP
路径,并进行跨域设置,withSockJS()
表示允许SockJS
覆盖configureMessageBroker
方法进行节点配置:enableSimpleBroker
配置广播节点,也就是服务器发送消息,客户端订阅就能接收消息的节点覆盖setApplicationDestinationPrefixes
方法,设置客户端向服务器发送消息的节点 覆盖setUserDestinationPrefix
方法,设置一对一通信的节点 捋一下广播节点是/topic
,一对一节点是/user
客户端向服务端发消息必须带上前缀/app
,然后不同后缀对应不同处理方法,服务端使用@MessageMapping
定义,和@RequestMapping
用法基本相同。
使用@MessageMapping
需要在配置类开启注解@EnableWebSocketMessageBroker
例如在控制器类(注解了@Controller
)中,有个注解了@MessageMapping("/hello")
的方法,那么客户端发送消息的URL为:/app/hello
,服务端会转给这个方法进行处理。
服务端向客户端发送消息有两种方式:广播和一对一。
广播地址前缀是topic
,一对一节点是/user
。
先说广播:
有两种发送消息方式,使用注解@SendTo
或SimpMessagingTemplate
对象。
@SendTo
注解修饰方法,该方法的返回值就是SendTo
发送的消息,但是@SendTo
不能单独使用,需要和@MessageMapping
结合使用。
因此这两个注解的分工是:
@MessageMapping
负责接收客户端发来的消息@SendTo
负责发送消息给对应的广播地址另一种是使用SimpMessagingTemplate
对象,该对象会自动注入,该对象有两种发送消息方法:
convertAndSend()
convertAndSendToUser()
以及对应的重载方法。
一对一节点:
同样可使用SimpMessagingTemplate
对象发送,或者注解@SendToUser
,用法和@SendTo
基本一样。
例子首先定义一个Message
实体类,用于服务端和客户端数据传输。
1 2 3 4 5 6 7 8 9 10 11 @Data public class Message { private String code; private String form; private String to; private String content; }
配置配置两个ServerEndpoint
,分别用于广播和点对点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints (StompEndpointRegistry registry) { registry.addEndpoint("/ws/public" ).setAllowedOriginPatterns("*" ).withSockJS(); registry.addEndpoint("/ws/private" ).setAllowedOriginPatterns("*" ).withSockJS(); } @Override public void configureMessageBroker (MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic" , "/user" ); registry.setApplicationDestinationPrefixes("/app" ); registry.setUserDestinationPrefix("/user" ); } }
广播1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Controller public class WSController { @Autowired private SimpMessagingTemplate wsTemplate; @MessageMapping("/hello") @SendTo("/topic/hello") public String hello (String message) { System.out.println("接受消息: " + message); Message msg = JSONUtil.toBean(message, Message.class); return "自定义消息:" + msg.getContent(); } }
这里服务端向客户端发送消息使用了注解@SendTo
,也可以去掉注释行,使用SimpMessagingTemplate
对象。
前端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <html > <head > <meta charset ="UTF-8" > <title > 等系统推消息</title > <script src ="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js" > </script > <script src ="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js" > </script > <script src ="https://code.jquery.com/jquery-3.2.0.min.js" integrity ="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin ="anonymous" > </script > <script type ="text/javascript" > var stompClient = null ; function setConnected (connected ) { document .getElementById ("connect" ).disabled = connected; document .getElementById ("disconnect" ).disabled = !connected; $("#response" ).html (); } function connect ( ) { var socket = new SockJS ("http://localhost:8080/ws/public" ); stompClient = Stomp .over (socket); stompClient.connect ({}, function (frame ) { setConnected (true ); console .log ('Connected: ' + frame); stompClient.subscribe ('/topic/hello' , function (response ) { var responseData = document .getElementById ('responseData' ); var p = document .createElement ('p' ); p.style .wordWrap = 'break-word' ; p.appendChild (document .createTextNode (response.body )); responseData.appendChild (p); }); },{}); } function disconnect ( ) { if (stompClient != null ) { stompClient.disconnect (); } setConnected (false ); console .log ("Disconnected" ); } function sendMsg ( ) { var content = document .getElementById ('content' ).value ; stompClient.send ("/app/hello" ,{},JSON .stringify ({'content' : content})); } </script > </head > <body onload ="disconnect()" > <noscript > <h2 style ="color: #ff0000" > Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2 > </noscript > <div > <div > <labal > 连接广播频道</labal > <button id ="connect" onclick ="connect();" > Connect</button > <labal > 取消连接</labal > <button id ="disconnect" disabled ="disabled" onclick ="disconnect();" > Disconnect</button > </div > <div id ="conversationDiv" > <labal > 广播消息</labal > <input type ="text" id ="content" /> <button id ="sendMsg" onclick ="sendMsg();" > Send</button > </div > <div > <labal > 接收到的消息:</labal > <p id ="responseData" > </p > </div > </div > </body > </html >
实现效果:
一对一1 2 3 4 5 6 7 8 9 10 11 @Controller public class WSController { @Autowired private SimpMessagingTemplate wsTemplate; @MessageMapping("/alone") public void pushToOne (@RequestBody Message msg) { wsTemplate.convertAndSendToUser(msg.getTo(), "/message" , msg.getContent()); } }
这里服务端向指定客户端发送消息使用的是SimpMessagingTemplate
对象,不能使用注解@SendToUser
,因为Message
对象决定了给哪一个客户端发送消息,使用注解无法提前确定客户端。
前端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 <html > <head > <meta charset ="UTF-8" > <title > 聊起来</title > <script src ="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js" > </script > <script src ="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js" > </script > <script src ="https://code.jquery.com/jquery-3.2.0.min.js" integrity ="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin ="anonymous" > </script > <script type ="text/javascript" > var stompClient = null ; function setConnected (connected ) { document .getElementById ("connect" ).disabled = connected; document .getElementById ("disconnect" ).disabled = !connected; $("#response" ).html (); } function connect ( ) { var socket = new SockJS ("http://localhost:8080/ws/private" ); stompClient = Stomp .over (socket); stompClient.heartbeat .outgoing = 20000 ; stompClient.heartbeat .incoming = 0 ; stompClient.connect ({}, function (frame ) { setConnected (true ); console .log ('Connected: ' + frame); stompClient.subscribe ('/user/' + document .getElementById ('user' ).value + '/message' , function (response ) { var responseData = document .getElementById ('responseData' ); var p = document .createElement ('p' ); p.style .wordWrap = 'break-word' ; p.appendChild (document .createTextNode (response.body )); responseData.appendChild (p); }); }); } function disconnect ( ) { if (stompClient != null ) { stompClient.disconnect (); } setConnected (false ); console .log ("Disconnected" ); } function sendMsg ( ) { var content = document .getElementById ('content' ).value ; var to = document .getElementById ('to' ).value ; stompClient.send ("/app/alone" , {'accessToken' : 'HWPO325J9814GBHJF933' }, JSON .stringify ({ 'content' : content, 'to' : to })); } </script > </head > <body onload ="disconnect()" > <noscript > <h2 style ="color: #ff0000" > Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2 > </noscript > <div > <div > <labal > 连接用户</labal > <input type ="text" id ="user" /> <button id ="connect" onclick ="connect();" > Connect</button > </div > <div > <labal > 取消连接</labal > <button id ="disconnect" disabled ="disabled" onclick ="disconnect();" > Disconnect</button > </div > <div id ="conversationDiv" > <labal > 发送消息</labal > <div > <labal > 内容</labal > <input type ="text" id ="content" /> </div > <div > <labal > 发给谁</labal > <input type ="text" id ="to" /> </div > <button id ="sendMsg" onclick ="sendMsg();" > Send</button > </div > <div > <labal > 接收到的消息:</labal > <p id ="responseData" > </p > </div > </div > </body > </html >
注意,前端代码在建立WebSocket连接时,同时订阅了/user/<>/message
这个地址,/user
开头,是一个一对一地址。其中<>
是用户自定义的名字,例如自定义为name1
,则该客户端订阅了/user/name1/message
这个地址,其他用户想要给该用户发消息,就通过这个name1
。
至于为什么地址是这种格式,看一下convertAndSendToUser
的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void convertAndSendToUser (String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException { Assert.notNull(user, "User must not be null" ); Assert.isTrue(!user.contains("%2F" ), "Invalid sequence \"%2F\" in user name: " + user); user = StringUtils.replace(user, "/" , "%2F" ); destination = destination.startsWith("/" ) ? destination : "/" + destination; super .convertAndSend(this .destinationPrefix + user + destination, payload, headers, postProcessor); }
可以看到,在最后一行,调用了convertAndSend
方法,发送地址拼接为this.destinationPrefix + user + destination
,其中this.destinationPrefix
就是在配置类WebSocketConfig
中设置的,这里this.destinationPrefix = "/user/"
。
user
是入参,指定了要发送的用户名,destination
也是入参,一个自定义参数,前后端代码保持一致即可。
服务端的注解@MessageMapping("/alone")
,不需要加/app
,但是客户端向服务端发送消息时,需要加上前缀变成:/app/alone
。
实现效果为:
备注还有一个注解,@SubscribeMapping
。
标记在方法上,用于将客户端以app
开头的订阅路由到对应的方法上,默认情况下,返回值会直接发送给客户端,不经过消息代理Broker
,是一次性的。可以通过@SendTo
或@SendToUser
修改这一行为,也就是先将返回值发给消息代理borker
,然后广播出去。
一般用于初始化数据,例如登入聊天室后,初始化在线人员列表和历史消息等。
例如@SubscribeMapping("/init")
,则前端代码通过stompClient.subscribe("/app/init")
,路由到该注解修饰的方法。
总结对于客户端或用户,有三种动作:
根据服务端设定的ServerEndpoint
地址,建立WebSocket连接 订阅地址,包括广播地址(以/topic
开头)和一对一地址(以/user
开头),这两种地址都自定义在配置类中 向服务端发送消息,所有消息发送地址均以/app
开头 对于服务端,有四种动作:
处理客户端的WebSocket连接请求,不需要代码自定义配置,设定好ServerEndpoint
地址即可 处理客户端的订阅请求,可以使用@SubscribeMapping
接收,但这是一次性的,较少使用。服务端需要配置好广播地址和一对一地址,保证正确发送消息,一般也不需要代码自定义配置订阅 接收客户端的消息,以/app
开头的消息,通过@MessageMapping
接收,并根据参数广播消息或一对一发送消息 向客户端发送消息,既可以是一个客户端向另一个客户端发送的消息,例如聊天室;也可以是服务端主动推送某些更新消息,例如天气应用,实时更新,通过注解@SendTo
和@SendToUser
,不过注解只能和@MessageMapping
结合,使用范围比较窄。SimpMessagingTemplate
对象使用范围广,推荐使用。