Play 2.6 WebSocket

WebSockets

WebSockets是一种支持全双工通信的套接字。现代的html5通过js api使得浏览器天生支持webSocket。但是Websockets在移动端以及服务器之间的通信也非常有用,在这些情况下可以复用一个已经存在的TCP连接。

处理WebSockets

一般Play通过action来处理http请求,但是WebSockets是完全不同的,没法使用action来处理。

Play处理WebSockets的机制是建立在Akka Streams之上的。一个WebSockets被抽象为Flow,接收的信息被添加到flow中,flow产生的信息将发送到客户端中。

在理论上flow可以被视为一个接收某些信息,对信息进行一些处理再将信息传输出去的实体,- there is no reason why this has to be the case, the input of the flow may be completely disconnected from the output of the flow. Akka stream为了这个目的提供了一个构造器,Flow.fromSinkAndSource。并且在处理WebSockets时,输入与输出往往是不连接的。

Play在WebSocket中提供了一些工厂方法用来构建WebSockets。

使用actors来处理WebSockets

我们可以使用Play的工具ActorFlow来将一个ActorRef转换为一个flow。这个工具接收一个函数,该函数将ActorRef转换为一个akka.actor.Props对象,这个对象描述了Play需要创建用来接收WebSocket链接的actor(翻译的不是很准确,可以参考原文和事例代码,原文function that converts the ActorRef to send messages to a akka.actor.Props object that describes the actor that Play should create when it receives the WebSocket connection):

import play.libs.streams.ActorFlow;
import play.mvc.*;
import akka.actor.*;
import akka.stream.*;
import javax.inject.Inject;

public class HomeController extends Controller {

    private final ActorSystem actorSystem;
    private final Materializer materializer;

    @Inject
    public HomeController(ActorSystem actorSystem, Materializer materializer) {
        this.actorSystem = actorSystem;
        this.materializer = materializer;
    }

    public WebSocket socket() {
        return WebSocket.Text.accept(request ->
                ActorFlow.actorRef(MyWebSocketActor::props,
                    actorSystem, materializer
                )
        );
    }
}

我们需要的actor

import akka.actor.*;

public class MyWebSocketActor extends AbstractActor {

    public static Props props(ActorRef out) {
        return Props.create(MyWebSocketActor.class, out);
    }

    private final ActorRef out;

    public MyWebSocketActor(ActorRef out) {
        this.out = out;
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder()
          .match(String.class, message ->
              out.tell("I received your message: " + message, self())
            )
          .build();
    }
}

所有从客户端接收到的消息都会被发送到actor中,任何由Play提供的消息都会被发送到客户端中。

检测WebSocket的关闭

当WebSocket关闭时,Play会自动的停止actor。这意味着你可以实现actor的popStop方法来处理这一情况。下面例子关闭了一些资源:

public void postStop() throws Exception {
    someResource.close();
}
关闭WebSocket

当actor处理WebSocket terminates时,Play会自动关闭WebSocket。所以如果需要关闭,发送一个PoisonPill毒药信息给你的actor

self().tell(PoisonPill.getInstance(), self());
拒绝一个WebSocket

有些情况可能需要判断是否接受一个WebSocket请求,这种情况下可以用acceptOrResult方法

public WebSocket socket() {
    return WebSocket.Text.acceptOrResult(request -> {
        if (session().get("user") != null) {
            return CompletableFuture.completedFuture(
                    F.Either.Right(ActorFlow.actorRef(MyWebSocketActor::props,
                            actorSystem, materializer)));
        } else {
            return CompletableFuture.completedFuture(F.Either.Left(forbidden()));
        }
    });
}
Note: WebSocket协议没有实现[同源策略](https://en.wikipedia.org/wiki/Same-origin_policy),所以没有办法抵御WebSocket跨站劫持(http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html)。为了保证WebSocket在劫持攻击下保持安全,需要检查request中的origin与服务端是否相同(防止跨域),并且进行验证(包括CSRF token)。如果没有通过验证可以拒绝该请求
异步接收WebSocket请求

返回值使用CompletionStage

处理不同类型的消息

目前只是展示了使用TextBuilder对字符串的处理。Play也支持通过BinaryBuilder来处理ByteSting,使用Json来将字符串解析为JSONNode。

public WebSocket socket() {
    return WebSocket.Json.accept(request ->
            ActorFlow.actorRef(MyWebSocketActor::props,
                    actorSystem, materializer));
}

Play同样支持将JSONNode转成更高级的对象。如果你有一个类,InEvent,代表输入事件,另一个类,OutEvent,代表输出事件

public WebSocket socket() {
    return WebSocket.json(InEvent.class).accept(request ->
            ActorFlow.actorRef(MyWebSocketActor::props,
                    actorSystem, materializer));
}

直接使用Akka streams来处理WebSocket

Actor并不总是合适的模型,特别是当WebSocket表现的更像是一个流。这时可以使用Akka Steams来处理。

import akka.stream.javadsl.*;

public WebSocket socket() {
    return WebSocket.Text.accept(request -> {
        // Log events to the console
        Sink<String, ?> in = Sink.foreach(System.out::println);

        // Send a single 'Hello!' message and then leave the socket open
        Source<String, ?> out = Source.single("Hello!").concat(Source.maybe());

        return Flow.fromSinkAndSource(in, out);
    });
}

一个WebSocket可以获取请求头部信息,这允许你读取标准的头部及session信息。但是无法获取请求体及响应信息。

这个例子中我们创建了一个sink将所有的信息打印到控制台中。为了发送信息,创建了一个source金金发送一个hello。我们也需要连接一个什么都不做的source,否则单个source会关闭flow,进而关闭链接。

可以在 https://www.websocket.org/echo.html上测试WebSocket,值需要将地址设为ws://localhost:9000

下面的例子会忽略所以的输入数据,在发送一个hello后关闭连接

public WebSocket socket() {
    return WebSocket.Text.accept(request -> {
        // Just ignore the input
        Sink<String, ?> in = Sink.ignore();

        // Send a single 'Hello!' message and close
        Source<String, ?> out = Source.single("Hello!");

        return Flow.fromSinkAndSource(in, out);
    });
}

另外一个例子会将输入打印成标准输出,然后使用一个mapped flow返回给客户端

public WebSocket socket() {
    return WebSocket.Text.accept(request -> {

        // log the message to stdout and send response back to client
        return Flow.<String>create().map(msg -> {
            System.out.println(msg);
            return "I received your message: " + msg;
        });
    });
}

配置帧长度

可以通过配置play.server.websocket.frame.maxLength或在启动时添加参数-Dwebsocket.frame.maxLength来配置帧的最大长度

sbt -Dwebsocket.frame.maxLength=64k run

HTTP的介绍基本就到这里了,后面会介绍Play中使用的模板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值