Java Web高級編程(四)
WebSocket
一、WebSocket的產生
用戶希望Web頁面可以進行交互,用於解決這個問題的技術是JavaScript,現在Web上有許多的可用的JavaScript框架,在使用極少的JavaScript的情況下就可以創建出豐富的單頁面Web——Ajax技術(異步JavaScript和XML)。
在采用了Ajax之後,瀏覽器中的Web應用程序可以與服務器端的組件進行通信,而不需要改變瀏覽器頁面或者刷新。這個通信過程不需要用戶知道,並且它可以用於向服務器發送新數據或者從服務器獲得新數據。
但是,瀏覽器只可以從服務器專區新的數據,但是瀏覽器並不知道數據什麽時候使用,只有服務器知道什麽時候有新數據發送到瀏覽器,而瀏覽器並不知道。
解決方法1,頻繁輪詢
頻繁輪詢服務器獲取新數據,以一個固定的頻率,通常是每秒一次,瀏覽器將發送Ajax請求到服務器查詢新數據。如果瀏覽器有新的數據發送到服務器,數據將被添加到輪詢請求中一同發送給瀏覽器(但是大量請求會被浪費)。
解決方法2,長輪詢
服務器只有在發送數據時才會響應瀏覽器(如果瀏覽器在服務器響應之前有新數據要發送,瀏覽器就必須要創建一個新的並行請求,或者終止當前的請求;TCP和HTTP規定了連接超時的情況;HTTP存在著強制的連接限制)。
解決方法3,分塊編碼
服務器可以在不聲明內容長度的情況下響應請求。在響應中,每個塊的開頭一次是:一個用於表示塊長度的數字、一系列表示塊擴展的可選字符和一個CRLF(回車換行)序列。接著是塊包含的數據和另一個CRLF。瀏覽器將創建一個連接到“下遊端點”的長生命連接,並且服務器將使用該連接以塊的方式向瀏覽器發送更新。
解決方法4,Applet和Adobe Flash
創建連接到服務器的普通TCP套接字連接,當瀏覽器有了新的數據要發送到服務器,它將由瀏覽器插件暴露出的JavaScript DOM函數調用Java或Flash方法,然後該方法吧數據轉發到服務器上。
解決方法5,WebSocket
WebSocket連接首先將使用非正常的HTTP請求以特定的模式訪問一個URL,WebSocket是持久的全雙工通信協議。在握手完成之後,文本和二進制消息將可以同時在兩個方向上進行發送,而不需要關閉和重新連接。
WebSocket的優點:
- 連接端口在80(ws)和433(wss),所以不會被防火墻阻塞。
- 使用HTTP握手,可以自然地集成到網絡瀏覽器和HTTP服務器上。
- 使用ping和pong保持WebSocket一直處於活躍狀態。
- 當消息啟動和它的內容到達時,服務器和客戶端都可以知道。
- WebSocket在關閉連接時會發送特殊的關閉消息。
- 可以支持跨區域連接。
二、WebSocket API
WebSocket並不只是在瀏覽器和服務器的通信,兩個以任何框架編寫、支持WebSocket的應用程序都可以創建WebSocket連接進行通信。
WebSocket的Java API包含在javax.websocket中,並指定了一組類和接口包含所有的常見功能。
客戶端API
客戶端API基於ContainerProvider類和WebSocketContainer、RemoteEndpoint和Session接口構建。
WebSocketContainer提供了對所有WebSocket客戶端特性的訪問,而ContainerProvider類聽了靜態的getWebSocketContainer方法用來獲取底層WebSocket客戶端的實現。
WebSocketContainer提供了4個重載的connectToServer方法,它們都將接受一個URI,用於連接遠程終端和初始化握手。
- 標註了@ClientEndpoint的任意類型的POJO
- 標註了@ClientEndpoint的任意類型的POJO的Class<?>
- Endpoint類的實例或者一個Class<? extends EndPoint>。
當握手完成是,connectToServer方法將返回一個Session。
其中WebSocket的Endpoint有3個方法,onOpen、onClose和onError,它們將在這些時間發生時進行調用。
而@ClientEndpoint類標註了@onOpen、@onClose和@onError的方法。
- @OnOpen方法可以有:一個可選的Session參數,一個可選的EndpointConfig參數。
- @OnClose方法可以有:一個可選的Session參數,一個可選的CloseReason參數。
- @OnError方法可以有:一個可選的Session參數,一個可選的Throwable參數。
- @OnMessage方法可以有:一個可選的Session參數,其它參數的組合。
這是一個WebSocket創建多人遊戲的服務器終端代碼:
public class TicTacToeServer { private static Map<Long, Game> games = new Hashtable<>(); private static ObjectMapper mapper = new ObjectMapper(); @OnOpen public void onOpen(Session session, @PathParam("gameId") long gameId, @PathParam("username") String username) { try { TicTacToeGame ticTacToeGame = TicTacToeGame.getActiveGame(gameId); if(ticTacToeGame != null) { session.close(new CloseReason( CloseReason.CloseCodes.UNEXPECTED_CONDITION, "This game has already started." )); } List<String> actions = session.getRequestParameterMap().get("action"); if(actions != null && actions.size() == 1) { String action = actions.get(0); if("start".equalsIgnoreCase(action)) { Game game = new Game(); game.gameId = gameId; game.player1 = session; TicTacToeServer.games.put(gameId, game); } else if("join".equalsIgnoreCase(action)) { Game game = TicTacToeServer.games.get(gameId); game.player2 = session; game.ticTacToeGame = TicTacToeGame.startGame(gameId, username); this.sendJsonMessage(game.player1, game, new GameStartedMessage(game.ticTacToeGame)); this.sendJsonMessage(game.player2, game, new GameStartedMessage(game.ticTacToeGame)); } } } catch(IOException e) { e.printStackTrace(); try { session.close(new CloseReason( CloseReason.CloseCodes.UNEXPECTED_CONDITION, e.toString() )); } catch(IOException ignore) { } } } @OnMessage public void onMessage(Session session, String message, @PathParam("gameId") long gameId) { Game game = TicTacToeServer.games.get(gameId); boolean isPlayer1 = session == game.player1; try { Move move = TicTacToeServer.mapper.readValue(message, Move.class); game.ticTacToeGame.move( isPlayer1 ? TicTacToeGame.Player.PLAYER1 : TicTacToeGame.Player.PLAYER2, move.getRow(), move.getColumn() ); this.sendJsonMessage((isPlayer1 ? game.player2 : game.player1), game, new OpponentMadeMoveMessage(move)); if(game.ticTacToeGame.isOver()) { if(game.ticTacToeGame.isDraw()) { this.sendJsonMessage(game.player1, game, new GameIsDrawMessage()); this.sendJsonMessage(game.player2, game, new GameIsDrawMessage()); } else { boolean wasPlayer1 = game.ticTacToeGame.getWinner() == TicTacToeGame.Player.PLAYER1; this.sendJsonMessage(game.player1, game, new GameOverMessage(wasPlayer1)); this.sendJsonMessage(game.player2, game, new GameOverMessage(!wasPlayer1)); } game.player1.close(); game.player2.close(); } } catch(IOException e) { this.handleException(e, game); } } @OnClose public void onClose(Session session, @PathParam("gameId") long gameId) { Game game = TicTacToeServer.games.get(gameId); if(game == null) return; boolean isPlayer1 = session == game.player1; if(game.ticTacToeGame == null) { TicTacToeGame.removeQueuedGame(game.gameId); } else if(!game.ticTacToeGame.isOver()) { game.ticTacToeGame.forfeit(isPlayer1 ? TicTacToeGame.Player.PLAYER1 : TicTacToeGame.Player.PLAYER2); Session opponent = (isPlayer1 ? game.player2 : game.player1); this.sendJsonMessage(opponent, game, new GameForfeitedMessage()); try { opponent.close(); } catch(IOException e) { e.printStackTrace(); } } }
服務器API
服務器API依賴於完整的客戶端API,它只添加了少數的類和接口,ServerContainer集成了WebSocketContainer,在Servlet環境中調用ServletContext.getAttribute("javax.websocket.server.ServerCOntainer")可以獲得ServerContainer實例,在獨立運行的應用程序中,需要按照特定的WebSocket實現的指令獲取ServerContainer實例。
不過,其實可以使用@ServerEndPoint標註服務器終端類即可,WebSocket實現可以掃描類的註解,並自動選擇和註冊服務器終端,容器在每次收到WebSocket連接時創建對應終端的實例,在連接關閉之後在銷毀實例。
在使用@ServerEndPoint,至少需要制定必須的value特性目標是該終端可以做出像的應用程序相對應的URL:
@ServerEndpoint("/ticTacToe/{gameId}/{username}")
如果應用程序部署到的地址為:http://www.example.org/app,那麽該服務器終端會響應地址:ws://www.example.org/app/ticTacToe/1/andre等,然後服務器終端中所有的@OnOpen、@OnClose、@OnError和@OnMessage方法都可以只用@PathParam(“{gameId}/{username}”)標註出一個可選的額外參數,並且其內容為改參數的值(1/andre)。
服務器終端中的時間處理方法將和客戶端中的時間處理方法一樣工作,區別只存在於握手階段,之後並沒有服務器和客戶端的差別。
Java Web高級編程(四)