應用程序通過WebSocket自行推送業務消息給Subscriber的實現

首先是使用 Spring Boot 構建包含 WebSocket 的工程。然後定義一個 Java-Config 的 WebSocket : 

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
		stompEndpointRegistry.addEndpoint("/platoEndpoint")  // 客戶端連接服務端的端點
			.setAllowedOrigins("*") // 不設置前臺連接時報 403 錯誤
			.withSockJS(); // 開啓SockJS支持
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableSimpleBroker("/backend"); // 客戶端訂閱地址的前綴
		registry.setApplicationDestinationPrefixes("/frontend"); // 客戶端請求服務端的前綴
	}
}

如果沒有客戶端需要發消息給服務端,或者懶得寫一個前綴,那麼 ApplicationDestinationPrefixes 也可以不設置。

剩下的就是定製一個暴露 WebSocket 接口的 Controller 即可:

/**
 * @MessageMapping:需要在此 value 的值前加上WebSocketConfig註冊的
 * 		ApplicationDestinationPrefixes(如果有),就構成了整個請求的路徑。
 * @SendTo: value 是指服務端將把消息發送到訂閱了這個路徑的所有客戶端
 * 
 * 用法:1. 根據當前配置,客戶端使用Stomp,stompClient.send("/frontend/input", {}, obj);
 * 		發送 obj 給服務端,服務端調用 showLog()處理,然後將處理的結果轉發給所有通過 
 * 		stompClient.subscribe('/backend/output', function (response) { ... })
 * 		訂閱了服務端暴露的接口的客戶端。           
 * 		2. 如果服務端需要在運行時,根據需要自行把信息推送給前端,則需要使用
 * 		SimpMessagingTemplate的convertAndSend()主動調用廣播端口,也就是@SendTo的值
 */
@RestController
public class PlatoWebSocketController {
	@MessageMapping("/request") 
	@SendTo("/backend/broadcast")  
	public String showLog(String fileName) {
		return "Message:::filename=" + fileName;
	}
	
	//----------服務端自己直接調用,達到主動推送消息給客戶端----------
	@Autowired
    private SimpMessagingTemplate template;
	
	@RequestMapping(value = "ws/message", method = RequestMethod.POST)
	public void pushMessage(@RequestBody String fileName){
		template.convertAndSend("/backend/broadcast", fileName);
	}
}

大多數都是將這個 Controller 直接標記爲 Spring 的 @Controller, 而我需要暴露一個 RESTFul 的接口,也就是這裏的pushMessage() ,所以就標記爲 @RestController。在這個方法裏面,藉助 SimpMessagingTemplate 可直接將 Rest 請求過來的信息廣播給所有訂閱了 "/backend/broadcast" 的客戶端。

我們的業務很簡單,頁面上傳了一個文件包,應用程序批處理文件包裏的所有文件,產生的所有日誌通過 WebSocket 實時地顯示在頁面上,讓用戶知曉處理過程。程序把日誌文本當作請求體,調用 pushMessage() 就可以單方面推送消息給客戶端。服務端創建 Socket Server, 監聽 Socket 消息的實現:

@Component
public class LogServer implements InitializingBean, DisposableBean {
	private Logger logger = LoggerFactory.getLogger(LogServer.class);	
	
	@Autowired
	private RestTemplate restTemp;
	@Autowired
	private Environment env;
	
	@Value("${websocket.port}")  
    private Integer wsPort;

	// 當Spring應用重啓時,需要關掉當前的websocket連接釋放端口,所以把websocket句柄設置爲實例變量
	private ServerSocket serverSocket;
	
	@Override
	public void destroy() {
		logger.info("Shutdown Socket Service.........");
		try {
			serverSocket.close();
		} catch (IOException e) {
			logger.error("There is an exception when close socket::{}", e);
		}
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		logger.info("進入 LogServer.afterPropertiesSet() 啓動 Socket 服務");
		
		String wsServerUrl = getWebSocketServerUrl();
		// 必須用線程讓 socket 不佔用主線程去監聽端口,否則主程序沒辦法起來
		new Thread() {
			public void run() {
				ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
				ServerSocket serverSocket = null;
				try {
					serverSocket = serverSocketFactory.createServerSocket(5000);
				} catch (IOException ignored) {
					logger.error("Unable to create server");
					System.exit(-1);
				}
				logger.info("LogServer running on port: {}", 5000);

				while (true) {
					List<String> list = new ArrayList<>();
					try {
						handleSocket(serverSocket, wsServerUrl, list);
					} catch (Exception e) {
						logger.error("Socket 線程被打斷,原因是::", e);
						throw new RuntimeException(e);
					}
				}
			}
		}.start();
	}

	private String getWebSocketServerUrl() {
		// 獲取應用部署的服務器,端口號,構造 pushMessage 的請求路徑
	}

	private void handleSocket(ServerSocket serverSocket, String wsServerUrl, List<String> list) throws Exception {
		try (Socket socket = serverSocket.accept()) {
			InputStream is = socket.getInputStream();
			BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
			String line = null;
			while ((line = br.readLine()) != null) {
				line = line.trim();
				int contentStartInd = line.indexOf(">") + 1;
				int contentEndInd = line.lastIndexOf("<");
				
				if (line.contains("message")) {
					list.add(line.substring(contentStartInd, contentEndInd));
				}
				if (list.size() == 3) {
					String message = StringUtils.join(list, " - ");
					
					// 用RestTemplate調用封裝Socket處理邏輯的REST接口
					restTemp.postForEntity(wsServerUrl, message, Integer.class);
					
					list = new ArrayList<>(); // 一條日誌處理完,清空容器準備接收下一條
				}
			}
		}
	}	
}

由於 Socket 服務啓動後會一直監聽給定的端口,佔用當前線程資源,所以需要新開一個線程去做,主線程繼續 Spring Boot 應用資源的加載。

客戶端多開一個 Log 的 Socket 輸出源,可以參照 Socket Logging 鏈接的實現,作爲測試的 main(),需要增加一行 handler 的 close() 方法,否則會報 java.net.SocketException: Connection reset 錯誤。

	public static void main(String argv[]) throws IOException {
		final Logger logger = Logger.getLogger(Test.class.getName());
	    Handler handler = new SocketHandler("192.168.1.82", 5000);
	    logger.addHandler(handler);
	    logger.log(Level.SEVERE, "Hello, 中國2");			    
	    logger.log(Level.INFO, "Welcome Home");
	    handler.close();
	}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章