ScalatraのAtmosphereはWebSocketで文字列しか送信できない

ScalatraでWebSocketをやるためのサポートとしてはAtmosphereが組み込まれていますが、Scalatra側の問題で文字列しか送信できないようになってしまっているという話。

trait ScalatraBroadcaster extends Broadcaster {

  private[this] val logger: Logger = Logger[ScalatraBroadcaster]
  protected var _resources: ConcurrentLinkedQueue[AtmosphereResource]
  protected var _wireFormat: WireFormat
  protected implicit var _actorSystem: ActorSystem

  def broadcast[T <: OutboundMessage](msg: T, clientFilter: ClientFilter)(implicit executionContext: ExecutionContext): Future[T] = {
    val selectedResources = _resources.asScala filter clientFilter
    logger.trace("Sending %s to %s".format(msg, selectedResources.map(_.uuid)))
    broadcast(_wireFormat.render(msg), selectedResources.toSet.asJava).map(_ => msg)
  }

}
trait WireFormat {
  def name: String
  def supportsAck: Boolean
  def parseInMessage(message: String): InboundMessage
  def parseOutMessage(message: String): OutboundMessage
  def render(message: OutboundMessage): String
}

これがScalatra+Atmosphereでメッセージを送信する場合に最終的に呼ばれるメソッドbroadcastの定義なんですが、継承しているインタフェースのBroadcaster.broadcastの第一引数はObject(Any)なのに対し、WireFormat.renderの型はStringに固定されてしまっています。

しかも、OutboundMessageにBinaryMessageって型があるのに、デフォルト実装では対応なし(空文字の送信になる)。そこはあきらめて、自作WireFormatをAtmosphereSupportを使用しているクラスでimplicit defしてあげましょう。

class BinaryWireFormat extends JacksonSimpleWireformat {
  override def render(message: OutboundMessage) = message match {
    case TextMessage(text) => text
    case JsonMessage(json) => renderJson(json)
    case BinaryMessage(binary) => Base64.getEncoder.encodeToString(binary)
    case _ => ""
  }
}

class MyScalatraServlet extends AtmosphereSupport {
  protected override implicit def wireFormat: WireFormat = new BinaryWireFormat

  atmosphere("/hoge") {
    new AtmosphereClient {
      def receive = {
        case Connected => println("connect")
        case Disconnected(disconnector, Some(error)) => println("disconnect")
        case Error(Some(error)) => println(error)
        case TextMessage(text) => send(new BinaryMessage(/* some Array[Byte] */))
        case JsonMessage(json) => send("not supported")
      }
    }
  }
}

atmosphereのソースも読んでみたが、renderの戻り値型をAnyに変更してArray[Byte]とかを突っ込んだ場合にどんな動きをするかはわからなかった。そこが分かれば、pull requestを出してもいいかもしれない。

mosquittoでMQTT over WebSocket

Ubuntu serverで、moquittoを使ったMQTTブローカを立てた。

備忘録として手順をメモしておく。

ポート開放

# この設定は再起動後に有効になる
sudo ufw enable
sudo ufw default DENY
sudo ufw allow 80/tcp
sudo ufw allow 1883/tcp

mosquittoのインストール

sudo apt-get install mosquitto
# publish, subscribeをローカルで試したい場合のみ
sudo apt-get install mosquitto-client

lighttpdのインストール

sudo apt-get install automake
sudo apt-get install libtool
sudo apt-get install pkg-config
sudo apt-get install libpcre3 libpcre3-dev
sudo apt-get install openssl libssl-dev
sudo apt-get install git-core
sudo apt-get install libbz2-dev
sudo apt-get install g++
sudo apt-get install unzip
sudo apt-get install make

git clone --recursive git://github.com/nori0428/mod_websocket.git
git clone --recursive https://github.com/lighttpd/lighttpd1.4.git
cd lighttpd1.4
git checkout lighttpd-1.4.33
cd ../mod_websocket
./bootstrap
./configure --with-lighttpd=/home/youku_s/lighttpd1.4
make install
cd ../lighttpd1.4
./autogen.sh
./configure --with-websocket=all
make
sudo make install

lighttpdの設定

/home/youku_s/lighttpd1.4/websocket.confを以下の内容で作成する。

server.document-root= "/home/youku_s/www/"
server.port = 80
server.modules = ( "mod_websocket" )

websocket.server = ("/mqtt" =>
                          (
                                "host" => "127.0.0.1",
                                "port" => "1883",
                                "type" => "binary",
                                "subproto" => "mqttv3.1"
                           ),
)
websocket.timeout=300
mimetype.assign = (
  ".html" => "text/html",
  ".txt" => "text/plain",
  ".jpg" => "image/jpeg",
  ".png" => "image/png"
)

lighttpdの起動

lighttpd -D -f websocket.conf

その他

WebSocketとして接続するには、80ポートで接続する。 それ以外は、1883ポートを使用する。

Apache HttpClientでファイルアップロード

MultipartEntityBuilderでEntityを組み立てる。

ファイルはMultipartEntityBuilder#addBinaryBodyでセットする。

複数ファイルを同じaddBinaryBodyのnameに設定することもできる。その場合、その名前が複数ファイルに関連付けられる。

HttpPost request = new HttpPost("http://localhost:8080");
HttpClient client = HttpClientBuilder.create().build();
HttpEntity entity = MultipartEntityBuilder.create()
    .addBinaryBody("file", file)
    .build();
request.setEntity(entity);
HttpResponse response = client.execute(request);

sbt compileで、一部の文字がSJISとして扱われてコンパイルに失敗するときの解決法

Windows環境でしか確認していません。

1. sbtconfig.txtに手を加える

以下を追加。

-Dfile.encoding=UTF-8

2. 環境変数JAVA_TOOL_OPTIONSに-Dfile.encoding=UTF-8を設定する。

1.で解決しなかった場合はこちら。(私がこのパターンでした)

Apache HttpClientでredirectに対応する

HttpClient 4.3.6環境では、HttpClientの生成で以下のようにする。

HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build();

redirectをカスタマイズする必要がある場合は、DefaultRedirectHandlerを拡張する。

DefaultHttpClient client = new DefaultHttpClient();
client.setRedirectHandler(new DefaultRedirectHandler() {                
    @Override
    public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
        boolean isRedirect = super.isRedirectRequested(response, context);
        if (!isRedirect) {
            int responseCode = response.getStatusLine().getStatusCode();
            if (responseCode == 301 || responseCode == 302) {
                return true;
            }
        }
        return isRedirect;
    }
});