Java Web高級編程(四)

WebSocketjava

1、WebSocket的產生web

用戶但願Web頁面能夠進行交互,用於解決這個問題的技術是JavaScript,如今Web上有許多的可用的JavaScript框架,在使用極少的JavaScript的狀況下就能夠建立出豐富的單頁面Web——Ajax技術(異步JavaScript和XML)。瀏覽器

在採用了Ajax以後,瀏覽器中的Web應用程序能夠與服務器端的組件進行通訊,而不須要改變瀏覽器頁面或者刷新。這個通訊過程不須要用戶知道,而且它能夠用於向服務器發送新數據或者從服務器得到新數據。服務器

可是,瀏覽器只能夠從服務器專區新的數據,可是瀏覽器並不知道數據何時使用,只有服務器知道何時有新數據發送到瀏覽器,而瀏覽器並不知道。websocket

解決方法1,頻繁輪詢網絡

頻繁輪詢服務器獲取新數據,以一個固定的頻率,一般是每秒一次,瀏覽器將發送Ajax請求到服務器查詢新數據。若是瀏覽器有新的數據發送到服務器,數據將被添加到輪詢請求中一同發送給瀏覽器(可是大量請求會被浪費)。session

解決方法2,長輪詢app

服務器只有在發送數據時纔會響應瀏覽器(若是瀏覽器在服務器響應以前有新數據要發送,瀏覽器就必需要建立一個新的並行請求,或者終止當前的請求;TCP和HTTP規定了鏈接超時的狀況;HTTP存在着強制的鏈接限制)。框架

解決方法3,分塊編碼異步

服務器能夠在不聲明內容長度的狀況下響應請求。在響應中,每一個塊的開頭一次是:一個用於表示塊長度的數字、一系列表示塊擴展的可選字符和一個CRLF(回車換行)序列。接着是塊包含的數據和另外一個CRLF。瀏覽器將建立一個鏈接到「下游端點」的長生命鏈接,而且服務器將使用該鏈接以塊的方式向瀏覽器發送更新。

解決方法4,Applet和Adobe Flash

建立鏈接到服務器的普通TCP套接字鏈接,當瀏覽器有了新的數據要發送到服務器,它將由瀏覽器插件暴露出的JavaScript DOM函數調用Java或Flash方法,而後該方法吧數據轉發到服務器上。

解決方法5,WebSocket

WebSocket鏈接首先將使用非正常的HTTP請求以特定的模式訪問一個URL,WebSocket是持久的全雙工通訊協議。在握手完成以後,文本和二進制消息將能夠同時在兩個方向上進行發送,而不須要關閉和從新鏈接。

WebSocket的優勢:

  1. 鏈接端口在80(ws)和433(wss),因此不會被防火牆阻塞。
  2. 使用HTTP握手,能夠天然地集成到網絡瀏覽器和HTTP服務器上。
  3. 使用ping和pong保持WebSocket一直處於活躍狀態。
  4. 當消息啓動和它的內容到達時,服務器和客戶端均可以知道。
  5. WebSocket在關閉鏈接時會發送特殊的關閉消息。
  6. 能夠支持跨區域鏈接。

2、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,用於鏈接遠程終端和初始化握手。

  1. 標註了@ClientEndpoint的任意類型的POJO
  2. 標註了@ClientEndpoint的任意類型的POJO的Class<?>
  3. Endpoint類的實例或者一個Class<? extends EndPoint>。

當握手完成是,connectToServer方法將返回一個Session。

其中WebSocket的Endpoint有3個方法,onOpen、onClose和onError,它們將在這些時間發生時進行調用。

而@ClientEndpoint類標註了@onOpen、@onClose和@onError的方法。

  1. @OnOpen方法能夠有:一個可選的Session參數,一個可選的EndpointConfig參數。
  2. @OnClose方法能夠有:一個可選的Session參數,一個可選的CloseReason參數。
  3. @OnError方法能夠有:一個可選的Session參數,一個可選的Throwable參數。
  4. @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)。

服務器終端中的時間處理方法將和客戶端中的時間處理方法同樣工做,區別只存在於握手階段,以後並無服務器和客戶端的差異。

相關文章
相關標籤/搜索