[Work/Class/Java with Processing/5_MVC]

アート系学生のためのJAVA with Processing - その5-2 ネットワーク上のModel-View-Controller : WebSocketプロトコロルの使用

導入

WebSocketとはネットワークプロトコルの一つで,5-1 Model-View-Contollerの設計の基礎で見たように,基本的にGETと同時にしかデータを送信・受信できないHTTPプロトコルの限界から新たに設計された,常時コネクションが張られており,サーバ側からクライアント側へプッシュ送信できるプロトコロルのことである.

従来マルチメディアアートプログラミング界隈では,投げっぱなしプロトコル(UDP)を採用したOpenSoundControllが多く使われていたが,多くの言語で,Webサーバ側,クライアント側での実装が登場し,かつ大量のクライアントをサーバにぶら下げることができるWebSocketが多く用いられるようになった.

ライブラリのProcessing IDEへの導入

通常サーバ側用のWebSocketライブラリの実装はApacheや他のWebアプリのためのWebサーバ用ライブラリと一体化しているために,サーバ側を書くのは環境設定そのものが面倒臭いのだが,幸いなことにProcessingでは(Webサーバ込みではなくWebSocket)単体でサーバとして使えるライブラリ(サーバサイドJava用ライブラリに単体利用できるように追加されたもの)が提供されていて,簡単に導入することができる.

プロセッシングのメニューから「スケッチ」→「ライブラリをインポート」→「ライブラリを追加」から「Websockets | Create websocket servers and clients, which makes it possible...」というライブラリをインストールする.

例外処理

後述のコード例で,ネットワークに関係する処理を行うとき以下のようにtry{}catch(){}で囲まれている処理部がある.これを例外処理という.

try{ 
   ...処理内容
}catch(Exception e){
   e.printStackTrace();
   System.exit(1);
}

例外処理とは,try{}で囲まれた部分で何かエラーが起きた時にcatch{}で囲まれた処理に飛ばし,その中で,失敗原因を特定し,失敗原因に適した処理を行う,ものである.

イベント関数などと同じく,Exceptionクラス(もしくはそれを継承したクラス)のオブジェクトインスタンスがシステムから自動的に発行されるので,その内部のエラーを見ることができる.コード例では原因の表示.printStackTrace()関数を実行した後,System.exit(1);で異常終了させている.もちろん終了させるだけでなくリトライ等も可能である.

特にネットワーク関係はエラーが起きやすいため,必ず例外処理を行うこと.

コード

サーバ側とクライアント側,2つのスケッチを作成すること.サーバ側のスケッチに入るBallModel.pdeは引き続き共通なので省略する.

サーバ側

サーバ側では「Model」を記述する.スプリッタで区切られたStringメッセージを受け取り,切り分けして各処理を行う.

// MVC_WebSocketServer.pde
import websockets.*;

BallModel ballModelArray[];
int windowWidth, windowHeight;

WebsocketServer websocketServer;


void setup(){
  int portNumber = 30005;
  String urlDirectory = "/BounceBall"; 
  // WebSocket URL is represented such as follows:
  // ws://sfdn-w1.sd.tmu.ac.jp:30005/BounceBall

  try{
    websocketServer = new WebsocketServer(this, portNumber, urlDirectory);
  }catch(Exception e){
    e.printStackTrace();
    System.exit(1);
  }
  
  windowWidth = 640;
  windowHeight = 480;
  ballModelArray = new BallModel[10];
   for(int i=0; i<ballModelArray.length; i++)
     ballModelArray[i] = 
       new BallModel(windowWidth, windowHeight);
}

void webSocketServerEvent(String receivedMessage){
  try{
    String sendString = "Command not Defined";
    String[] commandArray = receivedMessage.split("/", -1);
    if(commandArray[0].equals("step")){
      for(int i=0; i<10; i++){
       ballModelArray[i].updateParameters(); 
      }
      websocketServer.sendMessage("Parameter Updated");
      return;
    }
    else if(commandArray[0].equals("reset")){
      for(int i=0; i<10; i++){
        ballModelArray[i] = new BallModel(windowWidth, windowHeight); 
      }
    }
    else if(commandArray[0].equals("getBallInfo")){
      int index = Integer.parseInt(commandArray[1]);
      websocketServer.sendMessage(new String("BallInfo/" + index + "/Pos/" + 
        (int)ballModelArray[index].getBallPosition().getX() + "/" +
        (int)ballModelArray[index].getBallPosition().getY() + "/" +
        "Color/" +
        ballModelArray[index].getBallColor().getRed() + "/" +
        ballModelArray[index].getBallColor().getGreen() + "/" +
        ballModelArray[index].getBallColor().getBlue() + "/" +
        "Size/" +
        ballModelArray[index].getBallSize()));
      return;
    }
    else if(commandArray[0].equals("getNumOfBall")){
      websocketServer.sendMessage
        (new String(new Integer(ballModelArray.length).toString()));
      return;
    }
    websocketServer.sendMessage(sendString);
  }catch(Exception e){
    e.printStackTrace();
    System.exit(1);
  }
  return;
}

クライアント側

クライアント側では「View-Controller」を記述する.

// MVC_WebSocketClient.pde 

import processing.awt.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import websockets.*;

WebsocketClient websocketClient;
DocumentViewActionListener documentViewActionListener;
boolean inAnimation;

int xPos[], yPos[], rColor[], gColor[], bColor[], ballSize[];

void setup(){
  size(640, 480);
  String websocketServerURL = new String("ws://localhost:30005/BounceBall");
  websocketClient = new WebsocketClient(this, websocketServerURL);
  documentViewActionListener = new DocumentViewActionListener(this);
  inAnimation = false;
  this.frameRate(10);
  
  xPos = new int[10];
  yPos = new int[10];
  rColor = new int[10];
  gColor = new int[10];
  bColor = new int[10];
  ballSize = new int[10];
  for(int i=0; i<10; i++){
   xPos[i] = -1;
   yPos[i] = -1;
   rColor[i] = 0;
   gColor[i] = 0;
   bColor[i] = 0;
   ballSize[i] = 20;
  }
  
  JPanel panel = new JPanel();
  panel.setBounds(0, 0, 640, 50);
  panel.setLayout(new GridLayout(0, 4));

  JButton startButton = new JButton("Start");
  startButton.setActionCommand("start");
  startButton.addActionListener(documentViewActionListener);
  panel.add(startButton);
   
  JButton stopButton = new JButton("Stop");
  stopButton.setActionCommand("stop");
  stopButton.addActionListener(documentViewActionListener);
  panel.add(stopButton);
   
  JButton stepButton = new JButton("Step");
  stepButton.setActionCommand("step");
  stepButton.addActionListener(documentViewActionListener);
  panel.add(stepButton);
   
  JButton resetButton = new JButton("Reset");
  resetButton.setActionCommand("reset");
  resetButton.addActionListener(documentViewActionListener);
  panel.add(resetButton);
   
  Canvas canvas = (Canvas)surface.getNative();
  JLayeredPane pane = (JLayeredPane)canvas.getParent().getParent();
  pane.add(panel);
}

void draw(){
  fill(255, 255, 255, 60);
  rect(0, 0, width, height);
  
  if(inAnimation)
     websocketClient.sendMessage("step"); 
  
  for(int i=0; i<10; i++)
    websocketClient.sendMessage("getBallInfo/" + i); 
  
  for(int i=0; i<10; i++){
    fill(rColor[i], gColor[i], bColor[i]);
    ellipse(xPos[i], yPos[i], ballSize[i], ballSize[i]);
  }
}

void setActionCommand(String actionCommand){
  try{
    if(actionCommand.equals("start"))
      inAnimation = true;
    else if(actionCommand.equals("stop"))
      inAnimation = false;
    else if(actionCommand.equals("step"))
      websocketClient.sendMessage("step");
    else if(actionCommand.equals("reset"))
      websocketClient.sendMessage("reset");
  }catch(Exception e){
   e.printStackTrace();
   System.exit(1);
  }
}

void webSocketEvent(String receivedMessage){
  try{
   if(!receivedMessage.equals("Command not Defined")){
     String[] commandArray = receivedMessage.split("/", -1);
     if(commandArray[0].equals("BallInfo")){
       int index = Integer.parseInt(commandArray[1]);
       if(commandArray[2].equals("Pos")){
         xPos[index] = Integer.parseInt(commandArray[3]);
         yPos[index] = Integer.parseInt(commandArray[4]);
       }
    
       if(commandArray[5].equals("Color")){
         rColor[index] = Integer.parseInt(commandArray[6]);
         gColor[index] = Integer.parseInt(commandArray[7]);
         bColor[index] = Integer.parseInt(commandArray[8]);
       }
    
       if(commandArray[9].equals("Size"))
        ballSize[index] = Integer.parseInt(commandArray[10]);
     }
   }
  }catch(Exception e){
    e.printStackTrace();
    System.exit(1);
  }
}

class DocumentViewActionListener implements ActionListener{
  MVC_WebSocketClient documentView;
  DocumentViewActionListener(MVC_WebSocketClient documentView){
   this.documentView = documentView;
  }
  
  public void actionPerformed(ActionEvent e){
   documentView.setActionCommand(e.getActionCommand()); 
  }
}

複数クライアントへの対応

ProcessingのWebSocketライブラリは,まだ複数クライアントに,それぞれ別のメッセージを送るということができない.これはJavaのWebSocketがWebアプリケーションサーバフレームワークの中に含まれているものを,無理やり取り出してきて単体で使えるWebSocketライブラリに仕立て上げた,という経緯による.

しかし,WebSocketの仕様そのものは,WebSocketサーバインスタンス一つで複数クライアントにそれぞれ別のメッセージを送ることも可能であるように定められている.

詳しくは,ES2015で動的Webアプリの項目で行う.