要了解的內容:
sockjs,對於低版本的ie等不支持websocket的瀏覽器,采用js模擬websocket對象的辦法來實現兼容(其實也有輪詢的情況)。sockjs地址 https://github.com/sockjs/sockjs-client
stomp 協議,一種格式比較簡單且被廣泛支持的通信協議,spring4提供了以stomp協議為基礎的websocket通信實現。
------------------------------------------------------------------------------------------
然后,重點來了,spring實現websocket的大概原理是什么樣子的呢?spring 的websocket實現,實際上是一個簡易版的消息隊列(而且是主題-訂閱模式的),對於要發給具體用戶的消息,spring的實現是創建了一個跟用戶名相關的主題,實際上如果同一用戶登錄多個客戶端,每個客戶端都會收到消息,因此可以看出來,spring的websocket實現是基於廣播模式的,要實現真正的單客戶端用戶區分(單用戶多端登錄只有一個收到消息),只能依賴於session(相當於一個終端一個主題了)。消息的具體處理過程如何,先上一圖:
客戶端發送消息,服務端接收后先判斷該消息是否需要經過后台程序處理,也就是是否是application消息(圖中的/app分支),如果是,則根據消息的url路徑轉到controller中相關的處理方法進行處理,處理完畢后發送到具體的主題或者隊列;如果不是application消息,則直接發送到相關主題或者隊列,然后經過處理發送給客戶端。
因此在使用的時候,有了一開始的客戶端注冊到指定url,這個所謂的注冊到執行url的過程,實際就是客戶端跟服務端建立websocket連接的過程,連接建立之后,要發送或者接收什么消息都通過這一個websocket通信連接來完成,而不是每一個主題建立一個連接,這樣可以節省開銷。其中服務端代碼.withSockJS()的作用是聲明我們想要使用 SockJS 功能,如果WebSocket不可用的話,會使用 SockJS。
@Configuration @EnableWebSocketMessageBroker //在 WebSocket 上啟用 STOMP public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //webServer就是websocket的端點,客戶端需要注冊這個端點進行鏈接, registry.addEndpoint("/webServer").setAllowedOrigins("*").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // registry.setPathMatcher(new AntPathMatcher("."));//可以已“.”來分割路徑,看看類級別的@messageMapping和方法級別的@messageMapping registry.enableSimpleBroker("/topic","/user"); registry.setUserDestinationPrefix("/user/"); registry.setApplicationDestinationPrefixes("/app");//走@messageMapping } @Override public boolean configureMessageConverters(List<MessageConverter> messageConverters) { return true; } @Override public void configureWebSocketTransport(WebSocketTransportRegistration webSocketTransportRegistration) { } @Override public void configureClientInboundChannel(ChannelRegistration channelRegistration) { } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { // TODO Auto-generated method stub } @Override public void addArgumentResolvers(List<org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver> list) { } @Override public void addReturnValueHandlers(List<org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler> list) { } }
@EnableWebSocketMessageBroker 的作用是在WebSocket 上啟用 STOMP,registerStompEndpoints方法的作用是websocket建立連接用的(也就是所謂的注冊到指定的url),configureMessageBroker方法作用是配置一個簡單的消息代理。如果補充在,默認情況下會自動配置一個簡單的內存消息隊列,用來處理“/topic”為前綴的消息,但經過重載后,消息隊列將會處理前綴為“/topic”、“/user”的消息,並會將“/app”的消息轉給controller處理。
@RequestMapping("/myws") @Controller public class WebSocketController { @Resource private SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/hello") // @SendTo("/topic/hello")//會把方法的返回值廣播到指定主題(“主題”這個詞並不合適) public void toTopic(SocketMessageVo msg , String name) { System.out.println(msg.getName()+","+msg.getMsg()); this.simpMessagingTemplate.convertAndSend("/topic/hello",msg.getName()+","+msg.getMsg()); // return "消息內容:"+ msg.getName()+"--"+msg.getMsg(); } @MessageMapping("/message") // @SendToUser("/message")//把返回值發到指定隊列(“隊列”實際不是隊列,而是跟上面“主題”類似的東西,只是spring在SendTo的基礎上加了用戶的內容而已) public void toUser(SocketMessageVo msg ) { System.out.println(msg.getName()+","+msg.getMsg()); this.simpMessagingTemplate.convertAndSendToUser("123","/message",msg.getName()+msg.getMsg()); } @RequestMapping("/sendMsg") public void sendMsg(HttpSession session){ System.out.println("測試發送消息:隨機消息" +session.getId()); this.simpMessagingTemplate.convertAndSendToUser("123","/message","后台具體用戶消息"); } }
WebSocketConfig 中配置setApplicationDestinationPrefixes()的消息會被轉發到WebSocketController 中 @MessageMapping 相應方法進行處理。@SendTo("/topic/hello") 會把方法的返回值序列化為json串,然后發送到指定的主題,不用此注解,使用 simpMessagingTemplate.convertAndSend 效果相同;若為 @SendToUser("/message") 則為發送到指定的用戶隊列(實際隊列名字為/user/用戶名/原隊列名),不用此注解,使用 simpMessagingTemplate.convertAndSendToUser() 效果相同;
后台主動往前端推送消息,直接調用 simpMessagingTemplate.convertAndSendToUser() 跟 simpMessagingTemplate.convertAndSend() 即可將消息發往隊列或者主題。
前端代碼:
//建立websocket連接 function openWs(){ websocket = new SockJS("http://localhost:8080/autotest" + "/webServer"); var stompClient = Stomp.over(websocket); stompClient.connect({}, function(frame) { stompClient.subscribe('/topic/hello', function(data) { //訂閱消息 console.log("收到topic消息:"+data.body);//body中為具體消息內容 }); stompClient.subscribe('/user/' + 123 + '/message', function(message){ console.log("收到session消息:"+message.body);//body中為具體消息內容 }); }); document.getElementById("sendws").onclick = function() { stompClient.send("/app/message", {}, JSON.stringify({ name: "nane", msg: "發送的消息aaa" })); } } //關閉連接 function wsClose() { websocket.close(); }
代碼完成后運行效果如下:
可以看到連接建立握手的過程,以及訂閱成功后的消息打印,<<<為從服務端接收到的消息,>>>為往服務端發的消息。
--------------------------------------------------------------------------------
最后,websocket 跟輪詢,長連接相比有啥優勢,參見:https://www.zhihu.com/question/20215561
這里有一點不明,websocket跟長連接都是每個客戶端跟服務端建立了一個連接,為什么說長連接對服務端資源消耗嚴重,而不提websocket對服務端的消耗呢?是websocket協議更底層,只在物理鏈路上有個連接,並沒有實際消耗jvm的資源?有知道的大神請留言指教。