服務端主動推送技術☞WebSocket

服務端主動推送技術☞WebSocket

[toc]html

簡介

  • 什麼是WebSocket

WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通訊——容許服務器主動發送信息給客戶端前端

  • 實用場景

用到服務端主動推送的地方,都會使用WebSocket來實現,如:java

彈幕,網頁聊天系統,實時監控,股票行情推送等node

術語nginx

單播(Unicast):
  	點對點,私信私聊
  	

  廣播(Broadcast)(全部人):
  	遊戲公告,發佈訂閱
  
  多播,也叫組播(Multicast)(特意人羣):
  	多人聊天,發佈訂閱
複製代碼

webjargit

一、方便統一管理
  二、主要解決前端框架版本不一致,文件混亂等問題
  三、把前端資源,打包成jar包,藉助maven工具進行管理
複製代碼

既然用管理jar的方式管理js,那麼這個項目確定是沒有先後端分離的。
對於純前端項目,有其餘方式去管理js版本與依賴。就像maven管理jar那樣方便。web

編寫基本WebSocket服務端

pom

  • SpringBoot版本
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
複製代碼
  • WebSocket依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
複製代碼

配置類:WebSocketConfig

package com.example.websocket.websocketdemo01.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

import com.example.websocket.websocketdemo01.intecepter.HttpHandShakeIntecepter;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


    /** * 註冊端點,發佈或者訂閱消息的時候須要鏈接此端點 * setAllowedOrigins 非必須,*表示容許其餘域進行鏈接 * withSockJS 表示開始sockejs支持 */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        registry.addEndpoint("/endpoint-websocket")
// .addInterceptors(new HttpHandShakeIntecepter())
                .setAllowedOrigins("*").withSockJS();
    }

    /** * 配置消息代理(中介) * enableSimpleBroker 服務端推送給客戶端的路徑前綴 * setApplicationDestinationPrefixes 客戶端發送數據給服務器端的一個前綴 */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/topic", "/chat");
        registry.setApplicationDestinationPrefixes("/app");

    }


}
複製代碼

Controller

package com.example.websocket.websocketdemo01.controller.v1;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import com.example.websocket.websocketdemo01.model.InMessage;
import com.example.websocket.websocketdemo01.model.OutMessage;


@Controller
public class GameInfoController {


	//接收消息
	@MessageMapping("/v1/chat")
	//發送消息
	@SendTo("/topic/game_chat")
	public OutMessage gameInfo(InMessage message){
		System.out.println("GameInfoController->gameInfo");
		return new OutMessage(message.getContent());
	}
}
複製代碼

測試

  • 管理端

http://localhost:8080/v1/admin.htmlspring

  • 客戶端

http://localhost:8080/v1/index.html後端

鏈接上服務器後,管理端發送的內容會顯示在客戶端瀏覽器

小結

至此,咱們的基本的服務端就算編寫完畢了

當客戶端鏈接上/endpoint-websocket後,能夠往/v1/chat發送消息,而且監聽/topic/game_chat,服務端會將消息發往/topic/game_chat

任何客戶端,只要監聽了/topic/game_chat,就會收到這個推送

服務端主動推送消息

在上一章中,服務端經過接收前端的WebSocket請求進行響應,其實仍是一個請求響應推送,只不過這個過程當中連接不斷來。

當咱們使用WebSocket的時候,更多狀況下都是服務端被客戶端鏈接上後進行主動推送,這個時候該怎麼作呢?

@Controller
public class GameInfoController {

    @Autowired
    private SimpMessagingTemplate template;

    @GetMapping("/v1/chat/http")
    @ResponseBody
    public OutMessage gameInfoHttp(InMessage message) {
        System.out.println("gameInfoHttp");
        OutMessage outMessage = new OutMessage(message.getContent());
        template.convertAndSend("/topic/game_chat", new OutMessage(message.getContent()));
        return outMessage;
    }
}
複製代碼

只要使用SimpMessagingTemplate,就能夠往指定destination發送特定的數據,只要監聽了這個destination的客戶端都會收到

測試

訪問接口:http://localhost:8080/v1/chat/http?from=1&to=2&content=哇哈哈

能夠看到http://localhost:8080/v1/index.html收到的服務端的推送

關於客戶端鏈接地址:ws or http

由於咱們後端使用的是stomp協議,因此此時客戶仍舊使用http進行鏈接

若是要使用ws進行鏈接,那麼後端作修改

去掉withSocketJs
前端作修改


stompClient的構建方式發生了點變化

@SendToSimpMessagingTemplate

@SendTo不夠通用,固定發送給指定的訂閱者

SimpMessagingTemplate比較靈活

simpMessagingTemplate.convertAndSend("/topic/game_chat", new OutMessage(message.getContent()));
複製代碼

能夠動態的指定要發送給誰

SpringBoot對WebSocket的監聽

鏈接監聽

package com.example.websocket.websocketdemo01.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;

@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent>{

	@Override
	public void onApplicationEvent(SessionConnectEvent event) {
		StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
		System.out.println("【ConnectEventListener監聽器事件 類型】"+headerAccessor.getCommand().getMessageType());
		
		
	}

}
複製代碼

當客戶端鏈接的時候,會觸發CONNECT事件

訂閱監聽

package com.example.websocket.websocketdemo01.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;

@Component
public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent>{

	/** * 在事件觸發的時候調用這個方法 * * StompHeaderAccessor 簡單消息傳遞協議中處理消息頭的基類, * 經過這個類,能夠獲取消息類型(例如:發佈訂閱,創建鏈接斷開鏈接),會話id等 * */
	@Override
	public void onApplicationEvent(SessionSubscribeEvent event) {
		StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
		System.out.println("【SubscribeEventListener監聽器事件 類型】"+headerAccessor.getCommand().getMessageType());
		System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
		
	}

}
複製代碼

當客戶端鏈接的時候,會觸發SUBSCRIBE事件

取消監聽事件:SessionUnsubscribeEvent

客戶端斷開監聽

package com.example.websocket.websocketdemo01.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;


@Component
public class DissconnectEventListener implements ApplicationListener<SessionDisconnectEvent>{

	/** * 在事件觸發的時候調用這個方法 * * StompHeaderAccessor 簡單消息傳遞協議中處理消息頭的基類, * 經過這個類,能夠獲取消息類型(例如:發佈訂閱,創建鏈接斷開鏈接),會話id等 * */
	@Override
	public void onApplicationEvent(SessionDisconnectEvent sessionDisconnectEvent) {
		StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(sessionDisconnectEvent.getMessage());
		System.out.println("【SubscribeEventListener監聽器事件 類型】"+headerAccessor.getCommand().getMessageType());
		System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
	}
}
複製代碼

當客戶端鏈接的時候,會觸發DISCONNECT事件

獲取客戶端id

@Override
public void onApplicationEvent(SessionConnectEvent event) {
	StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
	System.out.println("【ConnectEventListener監聽器事件 類型】"+headerAccessor.getCommand().getMessageType());		
	System.out.println("simpSessionId\t"+headerAccessor.getHeader("simpSessionId"));
}
複製代碼

這個SimpSessionId會在客戶端鏈接、訂閱、斷線等狀況下獲取到,能夠用於標記客戶端

而在上面的監聽器中,咱們使用

System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
複製代碼

來獲取sessionId,這個好須要咱們編寫攔截器,將sessionId手動放到這個SessionAttributes,才能取到。

攔截器

package com.example.websocket.websocketdemo01.intecepter;

import java.util.Map;

import javax.servlet.http.HttpSession;

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;
public class HttpHandShakeIntecepter implements HandshakeInterceptor{

	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

		System.out.println("【握手攔截器】beforeHandshake");
		
		
		if(request instanceof ServletServerHttpRequest) {
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
			HttpSession session =  servletRequest.getServletRequest().getSession();
			String sessionId = session.getId();
			System.out.println("【握手攔截器】beforeHandshake sessionId="+sessionId);
			attributes.put("sessionId", sessionId);
		}
		
		return true;
	}

	
	
	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
		System.out.println("【握手攔截器】afterHandshake");
		
		if(request instanceof ServletServerHttpRequest) {
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
			HttpSession session =  servletRequest.getServletRequest().getSession();
			String sessionId = session.getId();
			System.out.println("【握手攔截器】afterHandshake sessionId="+sessionId);
		}
		
		
		
	}

}
複製代碼
  • 註冊攔截器

在攔截器中,咱們把sessionId放在了session中,此時WebSocket的監聽器就可以獲取到sessionId

headerAccessor.getSessionAttributes().get("sessionId")
複製代碼

點對點發送

客戶端訂閱的端,須要惟一,這個須要經過客戶端經過參數傳遞上來

template.convertAndSend("/chat/single/"+message.getTo(),
				new OutMessage(message.getFrom()+" 發送:"+ message.getContent()));
複製代碼

一樣的,對於組播來講,只要保證客戶端的訂閱頻道是同一組的就行。

通常對於的定義,都會經過業務來處理

Nginx反向代理WebSocket

http {

    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }

    upstream websocket {
		ip_hash;   #使用ip固定轉發到後端服務器
		server localhost:3100;  
		server localhost:3101;
        server localhost:3102;
	}


    server {
        listen 8020;
        location / {
            proxy_pass http://websocket;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade; # 聲明支持websocket
        }
    }
}



#http/2 nginx conf

#server{
# listen 443;
# server_name example.com www.example.com;
# root /Users/welefen/Develop/git/firekylin/www;
# set $node_port 8360;

# ssl on;
# ssl_certificate %path/ssl/chained.pem;
# ssl_certificate_key %path/ssl/domain.key;

# ssl_session_timeout 5m;
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
# ssl_session_cache shared:SSL:50m;
# ssl_dhparam %path/ssl/dhparams.pem;
# ssl_prefer_server_ciphers on;


# index index.js index.html index.htm;

# location ^~ /.well-known/acme-challenge/ {
# alias %path/ssl/challenges/;
# try_files $uri = 404;
# }

# location / {
# proxy_http_version 1.1;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header Host $http_host;
# proxy_set_header X-NginX-Proxy true;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_pass http://127.0.0.1:$node_port$request_uri;
# proxy_redirect off;
# }

# location = /development.js {
# deny all;
# }

# location = /testing.js {
# deny all;
# }

# location = /production.js {
# deny all;
# }


# location ~ /static/ {
# etag on;
# expires max;
# }
#}
#server {
# listen 80;
# server_name example.com www.example.com;
# rewrite ^(.*) https://example.com$1 permanent;
#}
複製代碼

階段1源碼

截止當前的全部代碼位於:碼雲

進階

使用ServerEndpoint的方式編寫

源碼

相關文章
相關標籤/搜索