Springを使用したWebコーディング

そのような相互作用の標準実装が存在するとすぐに言います



ただし、この記事はトピック「単一ページアプリケーションでのリモートサービスメソッドの単純な呼び出し」の続きであるため、前述のトピックで説明した(jrspc)アプローチのコンテキストでajaxをWebソケットに置き換えるには、ここで代替の相互作用スキームが必要です。



最初の記事では、ajaxを使用してサービスメソッドを呼び出すメカニズムについて説明しました。



この記事では、アプリケーションのビジネスロジックコードを変更せずにajaxをWebソケットに置き換えて、このメカニズムを実装する方法について説明します。



このような置換により、接続が高速になり(最後にテスト)、サーバーのメモリが節約され、サーバーからクライアントメソッドを呼び出す機能が追加されます。



デモのために、githubにソースコード記述した小さなチャットアプリケーションを作成しました

その例を使用して、この対話のクライアント部分とサーバー部分がどのように実装されるかを説明しようとします。

アプリケーションはtomcat 7.042サーバーで実行されます。

httpsおよびwss(証明書は未確認)をサポートし、サーバー上のログを保持しません。



サーバー側



SpringアプリケーションとのWebソケットインタラクションの編成中に生じた主な質問は、次の質問でした。Springコンポーネントが、セッションにアタッチされていないWebSocketServletクラスのcreateWebSocketInboundメソッドによって返されるStreamInboundオブジェクトからhttpセッションにアタッチされている可視領域から呼び出されるようにする方法ですか?



サーバーコンポーネントのメソッドを呼び出すために必要な機能を提供するには、何らかの形でStreamInboundの子孫から作業中のApplicationContextにアクセスする必要があります。



WebSocketServletまたはStreamInboundの後継で必要なコンポーネントを取得できるようにApplicationContextを認証しようとすると、初期化されないので失望します。これは完全に合法です。



Webソケットハンドラーからhttpセッションに関連付けられたSpringコンテキストからコンポーネントにアクセスするには、セッションスプリングBeanであり、アクセス先のストレージクラスの静的オブジェクトに格納されるオブジェクトを作成する必要があります。 StreamInboundの相続人になります。



このセッションオブジェクト(ClientManagerと呼びましょう)は、http接続の確立プロセス中に作成されます。



したがって、クライアントは、Webソケットを介してサーバーとの対話を開始する前に、1つのhttpハンドシェイク要求を作成する必要があり、その結果、ClientManagerのIDを取得する必要があります。



このリクエストの結果は、生成された生成ページにclientManagerIdを挿入するか、または静的ページからajaxリクエストを介してクライアントコードに転送できます(ここでは、ajaxを介したオプションが実装されます)。



このリクエストは、セッションコントローラーのinitializeClientManagerメソッドで処理されます。



@Controller @Scope("session") public class ClientManagerController { @Autowired private ClientManager clientManager; @RequestMapping(value = "/init", method = RequestMethod.POST) @ResponseBody private String initializeClientManager(HttpSession session) { JSONObject result = new JSONObject(); try{ boolean loged = ClientManagersStorage.checkClientManager(clientManager, session) ; result.put("loged", loged); result.put("clientManagerId", clientManager.getId()); }catch(Throwable th){ result.put("error", th.toString()); } return result.toString(); }
      
      







ClientManagersStorageは、セッションクライアントマネージャーのリポジトリです。このマネージャーには、マネージャーのnullのチェック、新しいマネージャーの作成、リポジトリへの追加、検索、削除のメソッドがあります。



 public class ClientManagersStorage { final static private Map<String, ClientManager> clientManagers = new ConcurrentHashMap <String, ClientManager>(); public static boolean checkClientManager(ClientManager clientManager, HttpSession session) { ClientManager registeredClientManager = clientManagers.get(clientManager.getId()); if (registeredClientManager == null) { clientManager.setSession(session); addClientManager(clientManager); registeredClientManager = clientManager; } return registeredClientManager.getUser() != null; } ... }
      
      







(セッションのライフサイクル管理については、以下で少し説明します)



ご覧のとおり、マネージャーは静的マップに格納され、キーは彼のhashCodeであり、ユーザーがページをリロードすると、同じマネージャーが彼に割り当てられます。



このマネージャーのIDは、応答のclientManagerId変数でクライアントに渡されます。



クライアントは、マネージャーのIDを受け取った後、接続を確立する要求の唯一のパラメーターでclientManagerIdを渡すことにより、Webソケット接続を開くことができます。



この接続を開く要求は、WebSocketConnectorServletクラスの抽象WebSocketServletの実装であるcreateWebSocketInboundメソッドで処理されます。



 @Override protected StreamInbound createWebSocketInbound(String paramString, HttpServletRequest request) { String clientManagerId = request.getParameter("clientManagerId"); ClientManager clientManager = ClientManagersStorage.findClientManager(clientManagerId); if(clientManager == null){ return new WebSocketConnection(null); } log.debug("new connection"); return new WebSocketConnection(clientManager); }
      
      





その中で、clientManagerIdはリクエストから取得され、ClientManagerはその上に配置され、ClientManagerがバインドされているWebSocketConnectionオブジェクト(StreamInbound)が作成されます。



ClientManagerはセッション1であり、「通常の」httpリクエストで作成されたため、すべてのスプリングビンは、そこに自動信頼されるApllicationContextを介して利用できます。ここでは、正しく初期化されます。



クライアントとの新しい接続を開くと、WebSocketConnectionクラスのonOpenメソッドが呼び出され、それに接続されたClientManagerは、オブジェクトID(ハッシュコード)を使用してこのWebSocketConnectionを接続マップに追加します。



  @Override protected void onOpen(WsOutbound outbound) { if(clientManager != null){ clientManager.addConnection(this); } }
      
      







(ユーザーが複数のウィンドウでアプリケーションを開くには、複数の接続のサポートが必要です。それぞれのウィンドウで独自のWebソケット接続が作成されます。)



接続を開いた後、クライアントは、WebSocketConnectionクラスのオーバーライドされたonTextMessageメソッドで処理されるサーバーメソッドへの呼び出し要求を送信できます。

  @Override protected void onTextMessage(CharBuffer message) throws IOException { try { String connectionId = String.valueOf(this.hashCode()); String request = message.toString(); clientManager.handleClientRequest(request, connectionId); } catch (Throwable th) { log.error("in onTextMessage: " + th); } }
      
      







ClientManagerクラスのhandleClientRequestメソッド-要求を処理し、結果を接続に書き込みます。



 @Autowired private RequestHandler requestHandler; public void handleClientRequest(String request, String connectionId) { log.debug("handleClientRequest request=" + request); log.debug("handleClientRequest user=" + getUser()); /** handleRequest - never throws exceptions ! */ JSONObject response = requestHandler.handleRequest(request, this); String responseJson = response.toString(); CharBuffer buffer = CharBuffer.wrap(responseJson); WebSocketConnection connection = connections.get(connectionId); try { connection.getWsOutbound().writeTextMessage(buffer); } catch (IOException ioe) { log.error("in handleClientRequest: in writeTextMessage: " + ioe); } }
      
      







requestHandlerは、リクエストの処理を担当する自動信頼コンポーネントです。

ApllicationContextは、サービスオブジェクトを見つけるための助けを借りて、それに結合されます。



そのhandleRequestメソッドは、前の記事のCommonServiceControllerクラスのprocessAjaxRequestメソッドと同様に、サービスコンポーネントを検索し、クライアントが必要とするメソッドを呼び出します。





これは、相互作用の一般的なスキームです。



次に、ClientManagerがhttpセッションで初期化された瞬間を詳しく見てみましょう。



セッションには、タイムアウト(デフォルトでは30分)で落ちるという特性があります。

これを回避するために、その値を最大に設定し、必要なときにセッションを無効にします。つまり、2つのケース:最初のケース-誰かがアプリケーションからではなくリクエストを行ったときと、クライアントがアプリケーションページを閉じたときです。



最初のケースは、初期化メソッドで直接処理されます。



  public class ClientManager{ public void setSession(HttpSession session) { /** session will be invalidated at connection removing */ session.setMaxInactiveInterval(Integer.MAX_VALUE);//69.04204112011317 years this.session = session; new Thread(new Runnable() { @Override public void run() { /** Giving time to client, for establish websocket connection. */ try {Thread.sleep(60000);} catch (InterruptedException ignored) {} /** if client not connected via websocket until this time - it is bot */ if (connections.size() == 0) {removeMe();} } }).start(); } private void removeMe() {ClientManagersStorage.removeClientManager(this);} ... }
      
      







WebSocketConnectionクラスのonCloseメソッドの2番目:



 public class WebSocketConnection{ @Override protected void onClose(int status) { if(clientManager != null){ clientManager.removeConnection(this); } } ... } public class ClientManager{ public void removeConnection(WebSocketConnection webSocketConnection) { String connectionId = getObjectHash(webSocketConnection); connections.remove(connectionId); if (connections.size() == 0) { log.debug("removeConnection before wait: connections.size()=" + connections.size()); /** may be client just reload page? */ try {Thread.sleep(waitForReloadTime);} catch (Throwable ignored) {} if (connections.size() == 0) { /** no, client leave us (page closed in browser)*/ ClientManagersStorage.removeClientManager(this); log.debug("client " + getId() + " disconnected"); } } } ... }
      
      







セッションは、ClientManagersStorageクラスのremoveClientManagerメソッドで無効になっています。



 public static void removeClientManager(ClientManager clientManager) { ClientManager removed = clientManagers.remove(clientManager.getId()); if(removed == null){return;} User user = removed.getUser(); if(user != null){ Broadcaster.broadcastCommand("userPanel.setLogedCount", UserService.logedCount.decrementAndGet()); } Broadcaster.broadcastCommand("userPanel.setOnlineCount", ClientManagersStorage.getClientManagersCount()); try { clientManager.getSession().invalidate(); clientManager.setSession(null); } catch (Throwable th) { log.error("at removeClientManager: " + th); } }
      
      





同じ方法を使用して、ページ訪問者の数が変更されたことをユーザーに通知します(クライアントでのこれらの通知の処理については、以下で説明します)。



サーバー上のイベントについてユーザーに通知するには、Broadcasterクラスを使用します。このクラスには、broadcastCommandとsendCommandToUserの2つのメソッドがあります。



 public class Broadcaster{ public static void broadcastCommand(String method, Object params) { for (ClientManager clientManager : ClientManagersStorage.getClientManagers().values()) { clientManager.sendCommandToClient(method, params); } } public static void sendCommandToUser(Long userId, String method, Object params) { List<ClientManager> userClientManagers = ClientManagersStorage.findUserClientManagers(userId); for(ClientManager clientManager: userClientManagers){ clientManager.sendCommandToClient(method, params); } } }
      
      







ClientManagerクラスのsendCommandToClientメソッドは次のように機能します。



 public void sendCommandToClient(String method, Object params) { for(WebSocketConnection connection: connections.values()){ sendCommandToClientConnection(connection, method, params); } } private void sendCommandToClientConnection(WebSocketConnection connection, String method, Object params) { JSONObject commandBody = new JSONObject(); if(params == null){params = new JSONObject();} commandBody.put("method", method); commandBody.put("params", params); CharBuffer buffer = CharBuffer.wrap(commandBody.toString()); try { connection.getWsOutbound().writeTextMessage(buffer); } catch (IOException ioe) { log.error("in sendCommandToClient: in writeTextMessage: " + ioe); } }
      
      







これで、サーバー部分で終わり、クライアント部分に進みます。



クライアント部



クライアント部分は、次の3つの機能を実装する必要があります。



1つ目は、セッションClientManagerを初期化するためのajaxでのハンドシェイク、2つ目はjsrpcリクエストの送信とそれらへの応答の受信のためのWebソケットトランスポート、3つ目はサーバーからのクライアントの関数の呼び出しです。



最初の部分は最も単純です:



Angularを使用しているため、$ httpを使用してajaxリクエストのhttpセッションを初期化します。



  var appName = "jrspc-ws"; var secured = document.location.protocol == "https:" ? "s" : ""; var HttpSessionInitializer = {url: "http"+secured+"://"+ document.location.host +"/"+appName+"/init"}; /** called from root-controller.js after its initialization */ HttpSessionInitializer.init = function($http) { $http.post(this.url, "").success(function(response){ if (response.error) { error(response.error); } else { loged = response.loged; Server.initialize("ws"+secured+"://"+ document.location.host +"/"+appName+"/ws?clientManagerId="+response.clientManagerId); if(loged){Listeners.notify("onLogin");} } }).error(function() {error("network error!");}); }
      
      







サーバーでは、このリクエストはClientManagerControllerクラスのinitializeClientManagerメソッドで処理されます。ClientManagerControllerクラスのコードは、上記のサーバー部分の説明で提供されています。



ソケット接続は、Server.initialize関数で初期化されます。



  connector.initialize = function(url) { connector.url = url; try { connector.connect(url); return true; } catch (ex) { p("in connector.initialize: " + ex); return false; } }
      
      







connector-Webソケット接続を担当する内部サーバーオブジェクト(完全なコードはws-connector.jsファイルにあります)



jrspcリクエストの生成を担当するws-connector.jsのコード:



  Server.socketRequests = {}; var requestId = 0; function sendSocket(service, method, params, successCallback, errorCallback, control) { if (!checkSocket()) {return;} requestId++; if(!params){params = [];} if(!isArray(params)){params = [params];} var data = { service : service, method : method, params : params, requestId : requestId }; Server.socketRequests["request_" + requestId] = { successCallback : successCallback, errorCallback : errorCallback, control : control }; if (control) {control.disabled = true;} var message = JSON.stringify(data); log("sendSocket: "+message); connector.socket.send(message); } ... Server.call = sendSocket;
      
      









ws-connector.jsのコード。リクエストに対する応答の処理とサーバーコマンドの処理を行います。



  connector.socket.onmessage = function(message) { var data = message.data; var response = JSON.parse(data); var requestId = response.requestId; if (requestId) {/** server return response */ var control = Server.socketRequests["request_" + requestId].control; if (control) {control.disabled = false;} if (response.error) { var errorCallback = Server.socketRequests["request_" + requestId].errorCallback; if (errorCallback) { try { errorCallback(response.error); } catch (ex) { error("in connector.socket.onmessage errorCallback: " + ex + ", data=" + data); } }else{ error(response.error); } } else { var successCallback = Server.socketRequests["request_" + requestId].successCallback; if (successCallback) { try { successCallback(response.result); } catch (ex) { error("in connector.socket.onmessage successCallback: " + ex + ", data=" + data); } } } delete Server.socketRequests["request_" + requestId]; } else { /** server call client or broadcast */ var method = eval(response.method); var params = response.params; try { method(params); } catch (ex) { error("in connector.socket.onmessage call method: " + ex + ", data=" + data); } } };
      
      







上記のフレームワークを使用すると、クライアント上の2つの関数( chat-c​​ontroller.js )で、チャット機能を担当するすべてのビジネスロジックを実装できます。



  self.sendMessage = function(command){ var message = {to: (self.sendPrivate ? self.privateTo : "all"), from: userPanel.user.login, text: self.newMessage, clientTime: new Date().getTime()}; Server.call("chatService", "dispatchMessage", message, function(){ self.newMessage = ""; self.$digest(); }, function(error){self.onError(error);}, command); } /** called from server */ self.onChatMessage = function (message){ message.isPrivate = (message.to != "all"); self.messages.push(message); self.$digest(); chatConsole.scrollTop = chatConsole.clientHeight + chatConsole.scrollHeight; }
      
      







および1つのサーバーメソッド:



  @Component public class ChatService extends AbstractService{ @Autowired private UserManager userManager; @Secured("User") @Remote public void dispatchMessage(ChatMessage message){ message.setServerTime(new Date().getTime()); String to = message.getTo(); if("ALL".equalsIgnoreCase(to)){ Broadcaster.broadcastCommand("chatPanel.onChatMessage", message); }else{ User fromUser = getUser(); message.setFrom(fromUser.getLogin()); User toUser = userManager.findByLogin(to); if(toUser == null){throw new RuntimeException("User "+to+" not found!");} Broadcaster.sendCommandToUser(toUser.getId(), "chatPanel.onChatMessage", message); Broadcaster.sendCommandToUser(fromUser.getId(), "chatPanel.onChatMessage", message); } } }
      
      





AjaxおよびWebSocketの1000リクエストのシリアルおよびパラレル送信による速度テスト:



順次:ajax(3474、3380、3377)ws(1299、1113、1054)

並列:ajax(1502、1515、1469)ws(616、637、632)



テストコード




  function testController($scope){ var self = $scope; self.maxIterations = 1000; self.testIterations = self.maxIterations; self.testStart = 0; self.testEnd = 0; self.testForSpeedSerial = function(command){ if(self.testStart == 0){self.testStart = now();} if(--self.testIterations <= 0){ var duration = now() - self.testStart; alert("testForSpeedSerial duration="+duration); self.testStart = 0; self.testIterations = self.maxIterations; return; } Server.call("userService", "testForSpeed", "", function(){ self.testForSpeedSerial(command); }, error, command); } self.testForSpeedParallelResponses = 0; self.testForSpeedParallel = function(command){ self.testStart = now(); for(var i = 0; i < self.testIterations; i++){ Server.call("userService", "testForSpeed", "", function(){ self.testForSpeedParallelResponses++ ; if(self.testForSpeedParallelResponses >= self.maxIterations){ var duration = now() - self.testStart; alert("testForSpeedParallel duration="+duration); self.testForSpeedParallelResponses = 0; } }, error, command); } } }
      
      





サーバーメソッドtestForSpeed:



  @Remote public void testForSpeed(){}
      
      









エラーのすべての批判と指摘は大歓迎です。



All Articles