WebSockets s Play Frameworkom i Akka

1. Pregled

Kada želimo da naši web klijenti održavaju dijalog s našim poslužiteljem, tada WebSockets može biti korisno rješenje. WebSockets održavaju trajnu full-duplex vezu. Ovaj daje nam mogućnost slanja dvosmjernih poruka između našeg poslužitelja i klijenta.

U ovom uputstvu naučit ćemo kako koristiti WebSockets s Akkom u Play Framework-u.

2. Postavljanje

Postavimo jednostavnu aplikaciju za chat. Korisnik će poslati poruke poslužitelju, a poslužitelj će odgovoriti porukom od JSONPlaceholder.

2.1. Postavljanje aplikacije Play Framework

Izgradit ćemo ovu aplikaciju koristeći Play Framework.

Slijedimo upute iz Uvoda u Play na Javi kako bismo postavili i pokrenuli jednostavnu aplikaciju Play Framework.

2.2. Dodavanje potrebnih JavaScript datoteka

Također, trebat ćemo raditi s JavaScriptom za skriptiranje na strani klijenta. To će nam omogućiti primanje novih poruka gurnutih s poslužitelja. Za to ćemo koristiti knjižnicu jQuery.

Dodajmo jQuery na dno app / views / index.scala.html datoteka:

2.3. Postavljanje Akke

Konačno, koristit ćemo Akka za obradu WebSocket veza na strani poslužitelja.

Idemo na graditi.sbt datoteku i dodajte ovisnosti.

Moramo dodati akka-glumac i akka-testkit ovisnosti:

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

Trebaju nam da bismo mogli koristiti i testirati Akka Framework kod.

Dalje, koristit ćemo akka struje. Pa dodajmo akka-potok ovisnost:

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

Na kraju, trebamo nazvati krajnju točku odmora od Akka glumca. Za ovo će nam trebati akka-http ovisnost. Kad to učinimo, krajnja točka vratit će JSON podatke koje ćemo morati deserijalizirati, pa moramo dodati akka-http-jackson ovisnost također:

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

I sad smo svi spremni. Pogledajmo kako pokrenuti WebSockets!

3. Rukovanje web utičnicama s Akka glumcima

Playov mehanizam za rukovanje WebSocketom izgrađen je oko Akka streamova. WebSocket je modeliran kao Flow. Dakle, dolazne WebSocket poruke unose se u tok, a poruke proizvedene protokom šalju se klijentu.

Da bismo obrađivali WebSocket pomoću glumca, trebat će nam uslužni program Play ActorFlow koji pretvara an Glumac do protoka. To uglavnom zahtijeva neki Java kôd, s malo konfiguracije.

3.1. Metoda kontrolera WebSocket

Prvo, trebamo Materijalizator primjer. Materializer je tvornica za motore za izvršavanje protoka.

Moramo ubrizgati ActorSystem i Materijalizator u kontroler app / controllers / HomeController.java:

privatni ActorSystemctorSystem; privatni materijalizator Materializer; @ Ubrizgajte javni HomeController (ActorSystemctorSystem, materijalizator materijalizatora) {this.actorSystem =ctorSystem; this.materializer = materijalizator; }

Dodajmo sada metodu kontrolera utičnice:

javna WebSocket socket () {return WebSocket.Json .acceptOrResult (this :: createActorFlow); }

Ovdje pozivamo funkciju acceptOrResult koja uzima zaglavlje zahtjeva i vraća budućnost. Vraćena budućnost je tok za obradu poruka WebSocket.

Umjesto toga, možemo odbiti zahtjev i vratiti rezultat odbijanja.

Sada, kreirajmo tok:

privatni CompletionStage<>> createActorFlow (Http.RequestHeader zahtjev) {return CompletableFuture.completedFuture (F.Either.Right (createFlowForActor ())); }

The F klasa u Play Framework definira skup pomagača u funkcionalnom programskom stilu. U ovom slučaju koristimo F.Ili.Tako je prihvatiti vezu i vratiti tok.

Recimo da smo željeli odbiti vezu kada klijent nije provjeren.

Za to bismo mogli provjeriti je li u sesiji postavljeno korisničko ime. A ako nije, odbijamo vezu s HTTP 403 Zabranjeno:

privatni CompletionStage<>> createActorFlow2 (Http.RequestHeader zahtjev) {return CompletableFuture.completedFuture (request.session () .getOptional ("username") .map (username -> F.Either.Desno (createFlowForActor ())) .orElseGet (() -> F.Either.Left (zabranjeno ()))); }

Koristimo F.Ili.Lijevo odbiti vezu na isti način na koji pružamo protok s F.Ili.Točno.

Na kraju, povezujemo tok s glumcem koji će obrađivati ​​poruke:

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

The ActorFlow.actorRef stvara protok kojim se rukuje Glasnik glumac.

3.2. The rute Datoteka

Dodajmo sada rute definicije za metode kontrolera u konf / rute:

GET / controllers.HomeController.index (zahtjev: Zahtjev) GET / chat kontroleri.HomeController.socket GET / chat / with / streams controllers.HomeController.akkaStreamsSocket GET / imovina / * kontrolori datoteka.Assets.versioned (path = "/ public" , datoteka: imovina)

Ove definicije ruta mapiraju dolazne HTTP zahtjeve na metode radnje kontrolera kako je objašnjeno u Routing u Play aplikacijama na Javi.

3.3. Implementacija glumca

Najvažniji dio klase glumaca je createReceive metoda koja određuje koje poruke glumac može obraditi:

@Override public Receive createReceive () {return receiveBuilder () .match (JsonNode.class, this :: onSendMessage) .matchAny (o -> log.error ("Primljena nepoznata poruka: {}", o.getClass ())) .izgraditi(); }

Glumac će proslijediti sve poruke koje se podudaraju s JsonNode razred do onSendMessage metoda rukovatelja:

privatna praznina onSendMessage (JsonNode jsonNode) {RequestDTO requestDTO = MessageConverter.jsonNodeToRequest (jsonNode); Niz poruke = requestDTO.getMessage (). ToLowerCase (); // .. processMessage (requestDTO); }

Tada će voditelj odgovoriti na svaku poruku pomoću processMessage metoda:

private void processMessage (RequestDTO requestDTO) {CompletionStage responseFuture = getRandomMessage (); responseFuture.thenCompose (this :: consumeHttpResponse) .thenAccept (messageDTO -> out.tell (MessageConverter.messageToJsonNode (messageDTO), getSelf ())); }

3.4. Konzumiranje API-ja za odmor s Akka HTTP-om

Poslat ćemo HTTP zahtjeve generatoru lažnih poruka na JSONPlaceholder Posts. Kada odgovor stigne, odgovor šaljemo klijentu tako da ga napišemo van.

Imajmo metodu koja poziva krajnju točku sa slučajnim id-om posta:

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

Također obrađujemo HttpResponse dobivamo od pozivanja službe kako bismo dobili JSON odgovor:

private CompletionStage konzumiratiHttpResponse (HttpResponse httpResponse) {Materijalizator materijalizator = Materializer.matFromSystem (getContext (). getSystem ()); return Jackson.unmarshaller (MessageDTO.class) .unmarshal (httpResponse.entity (), materializer) .thenApply (messageDTO -> {log.info ("Primljena poruka: {}", messageDTO); discardEntity (httpResponse, materializer); return messageDTO;}); }

The Pretvarač poruka class je uslužni program za pretvorbu između JsonNode i DTO-ovi:

javna statička MessageDTO jsonNodeToMessage (JsonNode jsonNode) {ObjectMapper mapper = novi ObjectMapper (); vratiti mapper.convertValue (jsonNode, MessageDTO.class); }

Dalje, moramo odbaciti entitet. The discardEntityBytes metoda pogodnosti služi u svrhu lakog odbacivanja entiteta ako za nas nema svrhu.

Pogledajmo kako odbaciti bajtove:

private void discardEntity (HttpResponse httpResponse, materijalizator materijalizatora) {HttpMessage.DiscardedEntity odbačen = httpResponse.discardEntityBytes (materijalizator); discarded.completionStage () .whenComplete ((gotovo, ex) -> log.info ("Entitet je u potpunosti odbačen!")); }

Nakon što smo obavili rukovanje WebSocketom, pogledajmo kako možemo postaviti klijenta za to pomoću HTML5 WebSockets.

4. Postavljanje klijenta WebSocket

Za našeg klijenta izradimo jednostavnu web-aplikaciju za chat.

4.1. Akcija kontrolera

Moramo definirati radnju kontrolera koja prikazuje stranicu indeksa. To ćemo staviti u klasu kontrolera app.controllers.HomeController:

javni indeks rezultata (zahtjev za Http.Request) {String url = routes.HomeController.socket () .webSocketURL (zahtjev); vrati se ok (views.html.index.render (url)); } 

4.2. Stranica s predlošcima

Sada, krenimo na app / views / ndex.scala.html stranicu i dodajte spremnik za primljene poruke i obrazac za hvatanje nove poruke:

 F Pošaljite 

Također ćemo morati proslijediti URL za akciju kontrolora WebSocket tako što ćemo ovaj parametar prijaviti na vrhu app / views / index.scala.htmlstranica:

@ (url: String)

4.3. Obrađivači događaja WebSocket u JavaScript-u

A sada možemo dodati JavaScript za obradu događaja WebSocket. Radi jednostavnosti, dodat ćemo JavaScript funkcije na dnu app / views / index.scala.html stranica.

Proglasimo voditelje događaja:

var webSocket; var messageInput; funkcija init () {initWebSocket (); } funkcija initWebSocket () {webSocket = novi WebSocket ("@ url"); webSocket.onopen = onOpen; webSocket.onclose = onClose; webSocket.onmessage = onMessage; webSocket.onerror = onError; }

Dodajmo same voditelje:

funkcija onOpen (evt) {writeToScreen ("POVEZANO"); } funkcija onClose (evt) {writeToScreen ("DISCONNECTED"); } funkcija onError (evt) {writeToScreen ("POGREŠKA:" + JSON.stringify (evt)); } funkcija onMessage (evt) {var полученData = JSON.parse (evt.data); appendMessageToView ("Poslužitelj", receivedData.body); }

Zatim ćemo za predstavljanje rezultata koristiti funkcije appendMessageToView i writeToScreen:

funkcija appendMessageToView (naslov, poruka) {$ ("# messageContent"). append ("

"+ title +": "+ poruka +"

");} funkcija writeToScreen (poruka) {console.log (" Nova poruka: ", poruka);}

4.4. Pokretanje i testiranje aplikacije

Spremni smo za testiranje aplikacije, pa pokrenimo je:

cd websockets sbt trčanje

Dok je aplikacija pokrenuta, možemo razgovarati s poslužiteljem posjetom // localhost: 9000:

Svaki put kad upišemo poruku i pritisnemo Poslati poslužitelj će odmah odgovoriti s nekima lorem ipsum iz JSON usluge rezerviranog mjesta.

5. Izravno rukovanje WebSocketsima s Akka Streams

Ako obrađujemo tok događaja iz izvora i šaljemo ih klijentu, onda to možemo modelirati oko Akka tokova.

Pogledajmo kako možemo koristiti Akka tokove u primjeru gdje poslužitelj šalje poruke svake dvije sekunde.

Počet ćemo s akcijom WebSocket u HomeController:

public WebSocket akkaStreamsSocket () {return WebSocket.Json.accept (request -> {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)); return Flow.fromSinkAndSource (in, out);}); }

The Izvor#krpelj metoda uzima tri parametra. Prvo je početno kašnjenje prije obrade prvog krpelja, a drugo je interval između uzastopnih krpelja. U gornjem isječku postavili smo obje vrijednosti na dvije sekunde. Treći parametar je objekt koji bi se trebao vratiti pri svakom označavanju.

Da bismo to vidjeli na djelu, moramo izmijeniti URL u indeks akcije i ukažite na to akkaStreamsSocket krajnja točka:

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

A sada, osvježavajući stranicu, vidjet ćemo novi unos svake dvije sekunde:

6. Ukidanje glumca

U jednom ćemo trenutku morati isključiti chat, bilo putem korisničkog zahtjeva ili putem vremenskog ograničenja.

6.1. Rukovanje prekidom glumca

Kako ćemo otkriti kada je WebSocket zatvoren?

Reprodukcija će automatski zatvoriti WebSocket kada glumac koji upravlja WebSocketom prestane. Tako se možemo nositi s ovim scenarijem primjenom Glumac # postStop metoda:

@Override public void postStop () baca izuzetak {log.info ("Messenger glumac zaustavio se na {}", OffsetDateTime.now () .format (DateTimeFormatter.ISO_OFFSET_DATE_TIME)); }

6.2. Ručno prekidanje glumca

Dalje, ako moramo zaustaviti glumca, možemo poslati PoisonPill glumcu. U našem primjeru aplikacije trebali bismo biti u mogućnosti obraditi zahtjev za zaustavljanjem.

Pogledajmo kako to učiniti u onSendMessage metoda:

privatna praznina onSendMessage (JsonNode jsonNode) {RequestDTO requestDTO = MessageConverter.jsonNodeToRequest (jsonNode); Niz poruke = requestDTO.getMessage (). ToLowerCase (); if ("stop" .equals (poruka)) {MessageDTO messageDTO = createMessageDTO ("1", "1", "Stop", "Zaustavljanje glumca"); out.tell (MessageConverter.messageToJsonNode (messageDTO), getSelf ()); self (). tell (PoisonPill.getInstance (), getSelf ()); } else {log.info ("Glumac primljen. {}", requestDTO); processMessage (requestDTO); }}

Kad primimo poruku, provjeravamo je li zahtjev za zaustavljanjem. Ako jest, šaljemo PoisonPill. U suprotnom, obrađujemo zahtjev.

7. Opcije konfiguracije

Možemo konfigurirati nekoliko opcija u smislu načina na koji treba postupati s WebSocketom. Pogledajmo nekoliko.

7.1. Duljina okvira WebSocket

WebSocket komunikacija uključuje razmjenu okvira podataka.

Duljina okvira WebSocket je podesiva. Imamo mogućnost prilagodbe duljine okvira našim zahtjevima primjene.

Konfiguriranje kraće duljine okvira može pomoći smanjiti napade uskraćivanja usluge koji koriste duge podatkovne okvire. Duljinu okvira za aplikaciju možemo promijeniti tako da odredimo maksimalnu duljinu u prijava.konf:

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

Ovu opciju konfiguracije možemo postaviti i određivanjem maksimalne duljine kao parametra naredbenog retka:

sbt -Dwebsocket.frame.maxLength = pokretanje 64k

7.2. Vrijeme čekanja u mirovanju veze

Prema zadanim postavkama, glumac kojeg koristimo za upravljanje WebSocketom prekida se nakon jedne minute. To je zato što poslužitelj Play na kojem je pokrenuta naša aplikacija ima zadano vrijeme čekanja od 60 sekundi. To znači da se sve veze koje ne prime zahtjev u šezdeset sekundi automatski zatvaraju.

To možemo promijeniti kroz opcije konfiguracije. Krenimo na našu prijava.konf i promijenite poslužitelj tako da nema praznog hoda:

play.server.http.idleTimeout = "beskonačno"

Ili ovu opciju možemo proslijediti kao argumente naredbenog retka:

sbt -Dhttp.idleTimeout = beskonačno pokretanje

To također možemo konfigurirati specificiranjem devSettings u graditi.sbt.

Opcije konfiguriranja navedene u graditi.sbt koriste se samo u razvoju, u proizvodnji će se zanemariti:

PlayKeys.devSettings + = "play.server.http.idleTimeout" -> "beskonačno"

Ako ponovno pokrenemo aplikaciju, glumac neće prekinuti.

Vrijednost možemo promijeniti u sekunde:

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

Više o dostupnim opcijama konfiguracije možemo saznati u dokumentaciji Play Framework.

8. Zaključak

U ovom uputstvu implementirali smo WebSockets u Play Framework s glumcima Akka i Akka Streams.

Zatim smo pogledali kako izravno koristiti Akka glumce, a zatim smo vidjeli kako se Akka Streamovi mogu postaviti za upravljanje WebSocket vezom.

Na strani klijenta koristili smo JavaScript za obradu naših WebSocket događaja.

Na kraju smo pogledali neke mogućnosti konfiguracije koje možemo koristiti.

Kao i obično, izvorni kod za ovu lekciju dostupan je na GitHubu.