WebSockets med Play Framework og Akka

1. Oversigt

Når vi ønsker, at vores webklienter opretholder en dialog med vores server, kan WebSockets være en nyttig løsning. WebSockets opretholder en vedvarende fuld-duplex-forbindelse. Det her giver os mulighed for at sende tovejsmeddelelser mellem vores server og klient.

I denne vejledning lærer vi, hvordan du bruger WebSockets med Akka i Play Framework.

2. Opsætning

Lad os oprette en simpel chatapplikation. Brugeren sender beskeder til serveren, og serveren svarer med en besked fra JSONPlaceholder.

2.1. Opsætning af Play Framework-applikationen

Vi bygger denne applikation ved hjælp af Play Framework.

Lad os følge instruktionerne fra Introduktion til Play i Java for at opsætte og køre en simpel Play Framework-applikation.

2.2. Tilføjelse af de nødvendige JavaScript-filer

Vi bliver også nødt til at arbejde med JavaScript til scripting på klientsiden. Dette gør det muligt for os at modtage nye meddelelser, der er skubbet fra serveren. Vi bruger jQuery-biblioteket til dette.

Lad os tilføje jQuery til bunden af app / visninger / index.scala.html fil:

2.3. Opsætning af Akka

Endelig bruger vi Akka til at håndtere WebSocket-forbindelserne på serversiden.

Lad os navigere til build.sbt fil og tilføj afhængighederne.

Vi er nødt til at tilføje akka-skuespiller og akka-testkit afhængigheder:

libraryDependencies + = "com.typesafe.akka" %% "akka-actor"% akkaVersion libraryDependencies + = "com.typesafe.akka" %% "akka-testkit"% akkaVersion

Vi har brug for disse for at kunne bruge og teste Akka Framework-koden.

Dernæst bruger vi Akka-streams. Så lad os tilføje akka-stream afhængighed:

libraryDependencies + = "com.typesafe.akka" %% "akka-stream"% akkaVersion

Endelig er vi nødt til at kalde et hvileendepunkt fra en Akka-skuespiller. Til dette har vi brug for akka-http afhængighed. Når vi gør det, returnerer slutpunktet JSON-data, som vi bliver nødt til at deserialisere, så vi skal tilføje akka-http-jackson afhængighed også:

libraryDependencies + = "com.typesafe.akka" %% "akka-http-jackson"% akkaHttpVersion libraryDependencies + = "com.typesafe.akka" %% "akka-http"% akkaHttpVersion

Og nu er vi klar. Lad os se, hvordan vi får WebSockets til at fungere!

3. Håndtering af WebSockets med Akka Actors

Play's WebSocket-håndteringsmekanisme er bygget op omkring Akka-streams. En WebSocket er modelleret som et flow. Så indgående WebSocket-meddelelser føres ind i strømmen, og meddelelser produceret af strømmen sendes til klienten.

For at håndtere en WebSocket ved hjælp af en skuespiller skal vi bruge Play-værktøjet SkuespillerFlow der konverterer en SkuespillerRef til en strøm. Dette kræver primært noget Java-kode med en lille konfiguration.

3.1. WebSocket Controller-metoden

For det første har vi brug for en Materializer eksempel. Materializer er en fabrik til stream-eksekveringsmotorer.

Vi er nødt til at indsprøjte ActorSystem og Materializer ind i controlleren app / controllere / HomeController.java:

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

Lad os nu tilføje en socket controller-metode:

offentlig WebSocket-sokkel () {returner WebSocket.Json .acceptOrResult (dette :: createActorFlow); }

Her kalder vi funktionen acceptOrResult der tager anmodningens overskrift og returnerer en fremtid. Den returnerede fremtid er et flow til håndtering af WebSocket-meddelelserne.

Vi kan i stedet afvise anmodningen og returnere et afvisningsresultat.

Lad os nu oprette strømmen:

private CompletionStage<>> createActorFlow (Http.RequestHeader anmodning) {returner CompletableFuture.completedFuture (F.Either.Right (createFlowForActor ())); }

Det F klasse i Play Framework definerer et sæt funktionelle hjælpere til programmeringsstil. I dette tilfælde bruger vi F.Enten. Lige for at acceptere forbindelsen og returnere strømmen.

Lad os sige, at vi ønskede at afvise forbindelsen, når klienten ikke er godkendt.

Til dette kunne vi kontrollere, om et brugernavn er indstillet i sessionen. Og hvis det ikke er, afviser vi forbindelsen med HTTP 403 Forbidden:

private CompletionStage<>> createActorFlow2 (Http.RequestHeader anmodning) {returner CompletableFuture.completedFuture (request.session () .getOptional ("brugernavn"). kort (brugernavn -> F. Enten.Højre (createFlowForActor ())). EllerElseGet (() -> F.Either.Left (forbudt ()))); }

Vi bruger F. enten venstre at afvise forbindelsen på samme måde som vi leverer en strøm med F.Enten. Ret.

Endelig forbinder vi strømmen til skuespilleren, der håndterer meddelelserne:

private Flow createFlowForActor () {return ActorFlow.actorRef (out -> Messenger.props (out), actorSystem, materializer); }

Det ActorFlow.actorRef skaber et flow, der håndteres af budbringer skuespiller.

3.2. Det ruter Fil

Lad os nu tilføje ruter definitioner for controllermetoderne i conf / ruter:

GET / controllers.HomeController.index (anmodning: Request) GET / chat controllers.HomeController.socket GET / chat / med / streams controllers.HomeController.akkaStreamsSocket GET / assets / * file controllers.Assets.versioned (path = "/ public" , fil: Aktiv)

Disse rutedefinitioner kortlægger indgående HTTP-anmodninger til controller-handlingsmetoder som forklaret i Routing i Play-applikationer i Java.

3.3. Skuespillerimplementeringen

Den vigtigste del af skuespillerklassen er createReceive metode som bestemmer hvilke beskeder skuespilleren kan håndtere:

@ Override public Receive createReceive () {return receiveBuilder () .match (JsonNode.class, this :: onSendMessage) .matchAny (o -> log.error ("Modtaget ukendt meddelelse: {}", o.getClass ())) .build (); }

Skuespilleren videresender alle meddelelser, der matcher JsonNode klasse til onSendMessage handler metode:

privat ugyldighed onSendMessage (JsonNode jsonNode) {RequestDTO requestDTO = MessageConverter.jsonNodeToRequest (jsonNode); Strengmeddelelse = requestDTO.getMessage (). ToLowerCase (); // .. processMessage (requestDTO); }

Derefter vil handler svare på enhver besked ved hjælp af processMessage metode:

privat ugyldig procesMessage (RequestDTO requestDTO) {CompletionStage responseFuture = getRandomMessage (); responseFuture.thenCompose (dette :: consumeHttpResponse) .thenAccept (messageDTO -> out.tell (MessageConverter.messageToJsonNode (messageDTO), getSelf ())); }

3.4. Forbruger Rest API med Akka HTTP

Vi sender HTTP-anmodninger til dummy-beskedgeneratoren på JSONPlaceholder Posts. Når svaret ankommer, sender vi svaret til klienten ved at skrive det ud.

Lad os have en metode, der kalder slutpunktet med et tilfældigt post-id:

private CompletionStage getRandomMessage () {int postId = ThreadLocalRandom.current (). nextInt (0, 100); returner Http.get (getContext (). getSystem ()) .singleRequest (HttpRequest.create ("//jsonplaceholder.typicode.com/posts/" + postId)); }

Vi behandler også HttpResponse vi kommer fra at ringe til tjenesten for at få JSON-svaret:

private CompletionStage consumeHttpResponse (HttpResponse httpResponse) {Materializer materializer = Materializer.matFromSystem (getContext (). getSystem ()); returner Jackson.unmarshaller (MessageDTO.class) .unmarshal (httpResponse.entity (), materializer). derefter Anvend (messageDTO -> {log.info ("Modtaget besked: {}", messageDTO); kassérEntity (httpResponse, materializer); returner messageDTO;}); }

Det MessageConverter klasse er et værktøj til konvertering mellem JsonNode og DTO'er:

offentlig statisk MessageDTO jsonNodeToMessage (JsonNode jsonNode) {ObjectMapper mapper = ny ObjectMapper (); returner mapper.convertValue (jsonNode, MessageDTO.class); }

Dernæst er vi nødt til at kassere enheden. Det kassérEntityBytes bekvemmelighedsmetode tjener formålet med let at kassere enheden, hvis den ikke har noget formål for os.

Lad os se, hvordan bortskaffes bytes:

private void discardEntity (HttpResponse httpResponse, Materializer materializer) {HttpMessage.DiscardedEntity kasseret = httpResponse.discardEntityBytes (materializer); discarded.completionStage () .whenComplete ((gjort, ex) -> log.info ("Enheden kasseret fuldstændigt!")); }

Efter at have gjort håndteringen af ​​WebSocket, lad os se, hvordan vi kan oprette en klient til dette ved hjælp af HTML5 WebSockets.

4. Opsætning af WebSocket Client

Lad os bygge en simpel webbaseret chatapplikation til vores klient.

4.1. Controller-handlingen

Vi er nødt til at definere en controllerhandling, der gengiver indekssiden. Vi sætter dette i controller-klassen app.controllers.HomeController:

offentligt resultatindeks (Http.Request anmodning) {String url = routes.HomeController.socket () .webSocketURL (anmodning); returner ok (views.html.index.render (url)); } 

4.2. Skabelonsiden

Lad os nu gå over til app / visninger / ndex.scala.html side og tilføj en container til de modtagne beskeder og en formular til at fange en ny besked:

 F Send 

Vi bliver også nødt til at videregive URL'en til handlingen WebSocket-controller ved at erklære denne parameter øverst på app / visninger / index.scala.htmlside:

@ (url: String)

4.3. WebSocket Event Handlers i JavaScript

Og nu kan vi tilføje JavaScript til at håndtere WebSocket-begivenhederne. For enkelheds skyld tilføjer vi JavaScript-funktionerne i bunden af app / visninger / index.scala.html side.

Lad os erklære begivenhedshåndtererne:

var webSocket; var messageInput; funktionsinit () {initWebSocket (); } funktion initWebSocket () {webSocket = ny WebSocket ("@ url"); webSocket.onopen = onOpen; webSocket.onclose = onClose; webSocket.onmessage = onMessage; webSocket.onerror = onError; }

Lad os tilføje håndtererne selv:

funktion onOpen (evt) {writeToScreen ("CONNECTED"); } funktion onClose (evt) {writeToScreen ("DISCONNECTED"); } funktion onError (evt) {writeToScreen ("FEJL:" + JSON.stringify (evt)); } funktion onMessage (evt) {var receivedData = JSON.parse (evt.data); appendMessageToView ("Server", receivedData.body); }

Derefter bruger vi funktionerne til at præsentere output appendMessageToView og skriv til skærm:

funktion appendMessageToView (titel, besked) {$ ("# messageContent"). tilføj ("

"+ titel +": "+ besked +"

");} funktion writeToScreen (besked) {console.log (" Ny besked: ", besked)}}

4.4. Kørsel og test af applikationen

Vi er klar til at teste applikationen, så lad os køre den:

cd websockets sbt run

Når applikationen kører, kan vi chatte med serveren ved at besøge // localhost: 9000:

Hver gang vi skriver en besked og rammer Sende serveren vil straks svare med nogle Lorem ipsum fra JSON Placeholder-tjenesten.

5. Håndtering af WebSockets direkte med Akka Streams

Hvis vi behandler en strøm af begivenheder fra en kilde og sender disse til klienten, kan vi modellere dette omkring Akka-streams.

Lad os se, hvordan vi kan bruge Akka-streams i et eksempel, hvor serveren sender meddelelser hvert andet sekund.

Vi starter med WebSocket-handlingen i HomeController:

offentlig WebSocket akkaStreamsSocket () {return WebSocket.Json.accept (anmodning -> {Sink in = Sink.foreach (System.out :: println); MessageDTO messageDTO = new MessageDTO ("1", "1", "Title", "Test Body"); Source out = Source.tick (Duration.ofSeconds (2), Duration.ofSeconds (2), MessageConverter.messageToJsonNode (messageDTO)); returner Flow.fromSinkAndSource (ind, ud);}); }

Det Kilde#kryds metode tager tre parametre. Den første er den indledende forsinkelse, før det første kryds behandles, og den anden er intervallet mellem på hinanden følgende flåter. Vi har indstillet begge værdier til to sekunder i ovenstående uddrag. Den tredje parameter er et objekt, der skal returneres ved hvert kryds.

For at se dette i aktion skal vi ændre URL'en i indeks handling og få det til at pege på akkaStreamsSocket slutpunkt:

String url = routes.HomeController.akkaStreamsSocket (). WebSocketURL (anmodning);

Og nu opdaterer vi siden, vi ser en ny post hvert andet sekund:

6. Afslutning af skuespilleren

På et eller andet tidspunkt skal vi lukke chatten, enten gennem en brugeranmodning eller gennem en timeout.

6.1. Håndtering af skuespillers afslutning

Hvordan registrerer vi, hvornår en WebSocket er blevet lukket?

Afspilning lukker automatisk WebSocket, når den skuespiller, der håndterer WebSocket, afsluttes. Så vi kan håndtere dette scenario ved at implementere Skuespiller # postStop metode:

@Override offentligt ugyldigt postStop () kaster Undtagelse {log.info ("Messenger-skuespiller stoppet ved {}", OffsetDateTime.now () .format (DateTimeFormatter.ISO_OFFSET_DATE_TIME)); }

6.2. Manuelt opsigelse af skuespilleren

Hvis vi skal stoppe skuespilleren, kan vi også sende en PoisonPill til skuespilleren. I vores eksempelapplikation skal vi kunne håndtere en "stop" -anmodning.

Lad os se, hvordan man gør dette i onSendMessage metode:

privat ugyldighed onSendMessage (JsonNode jsonNode) {RequestDTO requestDTO = MessageConverter.jsonNodeToRequest (jsonNode); Strengmeddelelse = requestDTO.getMessage (). ToLowerCase (); if ("stop" .equals (message)) {MessageDTO messageDTO = createMessageDTO ("1", "1", "Stop", "Stopping actor"); out.tell (MessageConverter.messageToJsonNode (messageDTO), getSelf ()); self (). tell (PoisonPill.getInstance (), getSelf ()); } andet {log.info ("Skuespiller modtaget. {}", requestDTO); processMessage (requestDTO); }}

Når vi modtager en besked, kontrollerer vi, om det er en stopanmodning. Hvis det er tilfældet, sender vi PoisonPill. Ellers behandler vi anmodningen.

7. Konfigurationsindstillinger

Vi kan konfigurere flere muligheder med hensyn til, hvordan WebSocket skal håndteres. Lad os se på nogle få.

7.1. WebSocket rammelængde

WebSocket-kommunikation involverer udveksling af datarammer.

WebSocket-rammelængden kan konfigureres. Vi har mulighed for at justere rammelængden til vores applikationskrav.

Konfiguration af en kortere rammelængde kan hjælpe med at reducere denial of service-angreb, der bruger lange datarammer. Vi kan ændre rammelængden for applikationen ved at angive den maksimale længde i application.conf:

play.server.websocket.frame.maxLength = 64k

Vi kan også indstille denne konfigurationsindstilling ved at angive den maksimale længde som en kommandolinjeparameter:

sbt -Dwebsocket.frame.maxLength = 64k kørsel

7.2. Forbindelse inaktiv timeout

Som standard afsluttes den skuespiller, vi bruger til at håndtere WebSocket, efter et minut. Dette skyldes, at Play-serveren, hvor vores applikation kører, har en standard inaktiv timeout på 60 sekunder. Dette betyder, at alle forbindelser, der ikke modtager en anmodning på tres sekunder, lukkes automatisk.

Vi kan ændre dette gennem konfigurationsindstillinger. Lad os gå over til vores application.conf og skift serveren, så den ikke har nogen ledig timeout:

play.server.http.idleTimeout = "uendelig"

Eller vi kan give indstillingen som kommandolinjeargumenter:

sbt -Dhttp.idleTimeout = uendelig kørsel

Vi kan også konfigurere dette ved at specificere devSettings i build.sbt.

Konfigurationsindstillinger specificeret i build.sbt bruges kun i udvikling, vil de blive ignoreret i produktionen:

PlayKeys.devSettings + = "play.server.http.idleTimeout" -> "uendelig"

Hvis vi genkører applikationen, afslutter skuespilleren ikke.

Vi kan ændre værdien til sekunder:

PlayKeys.devSettings + = "play.server.http.idleTimeout" -> "120 s"

Vi kan finde ud af mere om de tilgængelige konfigurationsindstillinger i Play Framework-dokumentationen.

8. Konklusion

I denne vejledning implementerede vi WebSockets i Play Framework med Akka-skuespillere og Akka Streams.

Vi fortsatte derefter med at se på, hvordan man bruger Akka-aktører direkte, og så, hvordan Akka Streams kan indstilles til at håndtere WebSocket-forbindelsen.

På klientsiden brugte vi JavaScript til at håndtere vores WebSocket-begivenheder.

Endelig så vi på nogle konfigurationsindstillinger, som vi kan bruge.

Som normalt er kildekoden til denne vejledning tilgængelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found