Websocket 就是在網頁 http protocol 之下,再增加的一層封裝協定,目的是讓 client 建立長時間的 socket 連線,在不重新建立連線的狀況下,我們可以傳送更多即時的資訊。
根據 WebSockets 的官方文件說明,scala play 2.5版已經調整為使用 Akka Streams 及 actors 來處理 WebSockets。
以最基本的 String 資料格式為例,跟一般的 http Action 一樣,websocket 也是以 Controller 為入口點。
WebSocketController 有兩個部分,index 是回傳一個簡單的 websocket 網頁 client,而 socket 就是主要處理 websocket 的部分。
// WebSocketController.scala
import play.api.mvc._
import play.api.libs.streams._
class WebSocketController @Inject()(implicit system: ActorSystem, materializer: Materializer) extends Controller {
def index = Action { implicit request =>
Ok(views.html.ws())
}
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef(out => MyWebSocketActor.props(out))
}
}
ActorFlow.actorRef(...) 可以被替代為 Akka Streams Flow[In, Out, _],但還是用 actors 撰寫會比較直接。
在 WebSocketController.scala 的下面,我們直接將處理 websocket 的 MyWebSocketActor 寫在下面,收到字串就會直接回傳 echo String: 加上原本收到的字串。
import akka.actor._
object MyWebSocketActor {
def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
class MyWebSocketActor(out: ActorRef) extends Actor {
def receive = {
case msg: String =>
Logger.debug(s"echo String: ${msg}")
out ! s"echo String: ${msg}"
}
}
在 routes 加上兩個 service uri
GET /ws controllers.WebSocketController.socket
GET /wstest controllers.WebSocketController.index
如果不要自己撰寫 websocket client 網頁,可以直接用 Websocket Echo Test 測試,把 Location: wss://echo.websocket.org 改為 ws://localhost:9000/ws,Use secure WebSocket (TLS) 取消勾選。點 Connect 以後,就可以對 ws server 發送文字訊息。
以下是自己撰寫 websocket 網頁的 sample: ws.scala.html
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
//var wsUri = "ws://localhost:9000/ws";
//var output;
function init()
{
//output = document.getElementById("output");
//testWebSocket();
}
function testWebSocket()
{
var wsUri= document.getElementById('wsUri').value;
websocket = new WebSocket(wsUri);
//websocket.binaryType = 'arraybuffer';
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}
function closeWebSocket() {
websocket.close();
}
function onOpen(evt)
{
writeToScreen("CONNECTED");
//doSend("WebSocket rocks");
}
function onClose(evt)
{
writeToScreen("DISCONNECTED");
}
function onMessage(evt)
{
console.log(evt);
if ( evt.data instanceof ArrayBuffer ) {
console.log( evt.data );
} else {
writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
}
//websocket.close();
}
function onError(evt)
{
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}
// function doSend(message)
// {
// writeToScreen("SENT: " + message);
// websocket.send(message);
// }
function doSendInput()
{
var message= document.getElementById('sendMessage').value;
writeToScreen("SENT: " + message);
websocket.send( message );
//websocket.send( JSON.stringify(JSON.parse(message)) );
//var byteArray = new Uint8Array(message);
//websocket.send( byteArray.buffer );
}
function writeToScreen(message)
{
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output = document.getElementById("output");
output.appendChild(pre);
}
function clearScreen()
{
var nodes = document.getElementById("output");
while (nodes.hasChildNodes()) {
nodes.removeChild(nodes.childNodes[0]);
}
}
window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<input id="wsUri" size="35" value="ws://localhost:9000/ws"></input>
<button id="close" onclick="javascipt:testWebSocket()">connect</button>
<button id="close" onclick="javascipt:closeWebSocket()">close</button>
<br/><br/>
<input id="sendMessage" size="35" value="測試 WebSocket!!!"></input>
<button id="send" onclick="javascipt:doSendInput()">Send</button>
<button id="clear" onclick="javascipt:clearScreen()">clear</button>
<div id="output"></div>
啟動 server 瀏覽網頁 http://localhost:9000/wstest 就可以測試 websocket
如果要將資料的格式換成 json,就稍微調整一下程式。
//WebSocketController.scala 把 String 換成 JsValue
def socketJs = WebSocket.accept[JsValue, JsValue] { request =>
ActorFlow.actorRef(out => MyWebSocketActor.props(out))
}
// 接收的資料改為 JsValue,並直接回傳原始的 json
class MyWebSocketActor(out: ActorRef) extends Actor {
def receive = {
case msg: JsValue =>
Logger.debug(s"echo JsValue: ${msg}")
out ! msg
}
}
routes 修改為 socketJs method
GET /ws controllers.WebSocketController.socketJs
GET /wstest controllers.WebSocketController.index
ws.scala.html 中發送資料的部分,以 JSON.parse 及 JSON.stringify 確認發送的資料是 json。
function doSendInput()
{
var message= document.getElementById('sendMessage').value;
writeToScreen("SENT: " + message);
//websocket.send( message );
websocket.send( JSON.stringify(JSON.parse(message)) );
}
測試的字串要改為 json
References
Binary websockets with Play 2.0 and Scala (and a bit op JavaCV/OpenCV)
Handling data streams reactively
沒有留言:
張貼留言