Server-sent events u Springu

Share

Zdravo, Java entuzijasti i profesionalni developeri. Ako ste se ikada suočili sa izazovima jednosmerne komunikacije server-klijent i tražite jasno rešenje, došli ste na pravo mesto. Tehnologija o kojoj ćemo govoriti u ovom kratkom blogu predstavlja lako i efektivno rešenje.
Zanimljiva je osnovna struktura problema. Na server strani imamo zakazane/nezakazane poslove koji se izvode i izvršavaju svoje zadatke u različitim vremenskim intervalima. Kada je određeni posao završen, obaveštava se strana klijenta (browser). To samo po sebi predstavlja čitav niz problema za rešavanje, a odličan način da se to uradi je korišćenje tehnologije Server-Sent Events.

Najbolji način za predstavljanje problema i rešenja koja Server-Sent Events nudi je kreiranje jednostavnog primera aplikacije. Aplikacija će biti Mehanizam za obaveštavanje sa brojnim poslovima u pozadini. Klijent (browser) će biti obavešten kada posao bude završen. Pre nego što započnemo sa kreiranjem našeg primera, objasnićemo neke osnove Server-Sent Events (SSE) tehnologije kako bi dalji tok priče bio jasniji.

Server-Sent Events tehnologija je deo HTML5 standarda i upravlja slanjem poruka klijentu sa servera putem HTTP veze. Veze su jednosmerne, što znači da samo server strana može poslati poruku klijentima. Važno je naglasiti da inicijalnu vezu uvek ostvaruje klijent (browser). Nakon toga, server je spreman za slanje poruka određenom povezanom klijentu. Ako se veza sa serverom izgubi, klijent će pokušati da se ponovo poveže, što je funkcija koju Server-Sent Events podržava out of the box.

Velika prednost Server-Sent Events je u tome što podržava sve glavne browser-e, osim Microsoft Internet Explorer-a i Edge-a, ali to se lako može rešiti korišćenjem polyfill biblioteka. Na osnovu mog ličnog iskustva, biblioteka Yaffle Event Source (https://github.com/Iaffle/EventSource) se dobro pokazala u ozbiljnom okruženju proizvoda. Što se tiče Spring Framework i Spring boot Server-Sent Events podrške, prisutna je od verzije 4.2, odnosno od verzije 1.3.

Pored pomenutih prednosti, Server-Sent Events nudi još mnogo više funkcija i na klijentskoj i na server strani, ali pošto ne želimo da previše zakomplikujemo stvari nećemo se njima baviti u ovom blogu.

Sada ću vam pokazati kako Server-Sent Events tehnologija zaista funkcioniše i kako se može koristiti u praksi. Kao što je već pomenuto, primer je pojednostavljeni mehanizam obaveštavanja – server šalje klijentu obaveštenja o poslu, klijent raščlanjuje ta obaveštenja i prikazuje ih.

Inicijalnu vezu kreira klijent (browser). Da bi se to postiglo, potrebno je napraviti EventSource objekat i dati URL server endpoint-a.

//create EventSource object that will be subscribed to 'new_notification' GET REST API
var eventSource = new EventSource('new_notification');

Nakon instanciranja objekta, klijent (browser) će poslati GET zahtev sa zaglavljem Accept sa value text/event-stream-om. S obzirom da je ovo samo normalan GET zahtev, URL može sadržati parametre kao i svaki drugi zahtev. Klijent je sada spreman da prima poruke od strane servera. Obratite pažnju da HTTP odgovor na klijentov GET zahtev mora da sadrži Content-Type zaglavlje sa kodiranim value text/event-stream-om i UTF-8-om. Odgovori su tekstualni i čuvaju se u data odeljku MessageEvent objekta.

Da bi mogla da obradi događaje na klijentskoj strani, aplikacija mora da ima EventSource.onmessage event handler koji se poziva po prijemu poruke sa određene URL adrese.

//Event handler that processes message events from specified URL
eventSource.onmessage = function(e) {
    var notification = JSON.parse(e.data);		
    document.getElementById("notificationDiv").innerHTML += notification.text + " at " + new Date(notification.time) + "<br/>";
};

Poruka primljena od strane servera ne mora biti jednostavan string. Server može da šalje JSON stringove koji se mogu raščlaniti pomoću JSON.parse u JavaScript objekat.

Sada treba da obavimo cleanup. Veza se zatvara jednostavnom upotrebom EventSource metode zatvaranja.

//Method that closes the connection
eventSource.close();

Klijent (browser) će pokušati da održi otvorenu vezu što je duže moguće. U slučaju neuspelog povezivanja, klijent će pokušati da se ponovo poveže nakon 3 sekunde. Server može promeniti vreme ponovnog povezivanja.

Ispod možemo, pomoću EventSource objekta i URL-a servera, videti kompletan klijentov kod sa početnim povezivanjem i pretplatom, event handler metodu koja će obraditi poruke primljene sa servera, kao i cleanup.

<!DOCTYPE html>
<html>
<head>
<title>Job Notifications</title>
<script>
var subscribe = function() {  
  var eventSource = new EventSource('new_notification');

  eventSource.onmessage = function(e) {    
    var notification = JSON.parse(e.data);               
    document.getElementById("notificationDiv").innerHTML += notification.text + " at " + new Date(notification.time) + "<br/>";
  };
}
window.onload = subscribe;
window.onbeforeunload = function() {
  eventSource.close();
 }
</script>
</head>
<body>
  <h1>Notifications: </h1>
  <div id="notificationDiv"></div>
</body>
</html>

Sada je vreme da vidimo kako stoje stvari na server/backend strani. Kao primer, iskoristićemo Spring Boot aplikaciju da bismo pojednostavili proces konfiguracije i usredsredili se samo na Server-Sent Events tehnologiju.

Za početak ćemo napraviti POJO sa informacijama o obaveštenjima.

public class Notification {

        public Notification(String text, Date time) {
               super();  
               this.text = text;
               this.time = time;
        } 

        ...

        public static Integer getNextJobId() {
              return ++jobId;
        } 

        private String text; 
        private Date time; 
        private static Integer jobId = 0; 
       
}

Nakon što smo napravili POJO za obaveštenja, treba da kreiramo uslugu zakazivanja koja simulira realno ponašanje servera. Server će slati poruke obaveštenja o pokretanju i završetku posla na svake 4 sekunde. Možemo je implementovati pomoću Spring-ovog ApplicationEventPublisher-a.

@Service
public class NotificationJobService {

        public final ApplicationEventPublisher eventPublisher;
        
        public NotificationJobService(ApplicationEventPublisher eventPublisher) {
                this.eventPublisher = eventPublisher;
        }

        @Scheduled(fixedRate = 4000, initialDelay = 2000)
        public void publishJobNotifications() throws InterruptedException {
                Integer jobId = Notification.getNextJobId();
                Notification nStarted = new Notification("Job No. " + jobId + " started.", new Date());
                this.eventPublisher.publishEvent(nStarted);
                Thread.sleep(2000);
                Notification nFinished = new Notification("Job No. " + jobId + " finished.", new Date());
                this.eventPublisher.publishEvent(nFinished);
        }
}

Sledeći korak je pravljenje REST Controller-a pomoću getNewNotification GET metode koja će hendlovati EventSource GET zahteve od klijenata. Da bi razlikovala klijente međusobno, getNewNotification metoda treba da vrati instancu SSEmitter-a. Svaka klijentska veza ima svoju instancu SSEmitter-a. Da bismo upravljali svim ovim instancama emitera, čuvamo ih na listi i uklonimo ih ako su završene ili istekle. Spring Boot sa Tomcat 9 drži vezu otvorenom 30 sekundi, ali se može podesiti putem konstruktora emitera, ili u datoteci application.properties (spring.mvc.async.request-timeout=60000 #trajanje veze 60 sekundi).

Druga Rest Controller metoda, nazvana onNotification, označena je sa @EventListener i preslušava događaje objavljene od strane eventPublisher u NotificationJobService klasi. U ovoj metodi ćemo pregledati listu emitera i poslati objavljeno obaveštenje. Ovde možemo videti da će instanca obaveštenja automatski biti pretvorena u JSON string. Osim SseEmitter objekta, možemo poslati i SseEmitter builder koji nudi dodatna podešavanja atributa.

@RestController
public class NotificationRestController {

        private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

        @GetMapping("/new_notification")
        public SseEmitter getNewNotification() {
                SseEmitter emitter = new SseEmitter();
                this.emitters.add(emitter);

                emitter.onCompletion(() -> this.emitters.remove(emitter));
                emitter.onTimeout(() -> {
                        emitter.complete(); 
                        this.emitters.remove(emitter);
                });

                return emitter;
        }

        @EventListener
        public void onNotification(Notification notification) {
                List<SseEmitter> deadEmitters = new ArrayList<>();
                this.emitters.forEach(emitter -> {
                        try {
                               emitter.send(notification);
                        } catch (Exception e) {
                               deadEmitters.add(emitter);
                        }
                });
                this.emitters.remove(deadEmitters);
        }

}

Kao što vidimo kada pokrenemo primer koda, backend strana šalje obaveštenja na svake 2 sekunde, a browser ih prima. Server-Sent Events tehnologija nam nudi jednostavan način za primanje obaveštenja / rezultata sa server strane bez potrebe za previše složenim kodom.

Ako pregledamo primer pomoću Chrome Developer Tool-a (Network tab), na prvoj slici možemo videti „pretplatu“ klijenta na server endpoint API (‘new_notification’ GET REST API) nakon kreiranja EventSource objekta.

Img001

Druga slika nam pokazuje u kom obliku klijent prima poruke sa servera. Kao što vidimo, SSEmitter-ova metoda slanja automatski konvertuje objekat obaveštenja u JSON string.

Img001

Konačno, poslednja slika prikazuje poruke u browser-u nakon što JSON.parse analizira svaku u EventSource’s onMessage Event handler-u.

Img001

Bilo mi je veliko zadovoljstvo da radim sa Server-Sent Events u Springu. Nadam se da će vama ovaj članak pomoći da započnete sa tehnologijom Server-Sent Events. Naravno, svaki projekat nosi svoje izazove i specifičnosti, ali bi osnove i suština tehnologije trebalo da budu iste.

Tagovi:  #java   #java_spring   #server_sent_events   #spring   #spring_boot   #spring_framework

Share

Prijavi se da prvi dobijaš nove blogove i vesti.

Оставите одговор