Jax-RS 2 ve LongPolling destekli Chat uygulaması
LongPolling; Reverse Ajax, Comet gibi isimlerle de bilinen, Javascript destekli Web tarayıcılarda sorunsuz bir biçimde çalışabilen bir Push yöntemidir.
HTML 5 ile birlikte, SSE ve Websocket gibi ileri Push teknikleri hali hazırda bulunsada, gerek teknolojilerin halen geliştirilme sürecinde oluşu, gerek se Web tarayıcıların tam olarak desteklemeyişi, LongPolling ve benzeri teknikleri hüküm sahibi kılıyor.
LongPolling tekniğinde, web tarayıcı sunucuya bir istekte bulunur, ve bu istek sunucuda hazır bir yanıt bulunana kadar sunucu üzerinde askıda kalır. Askıda kalan HTTP isteği, bir yanıta (resource) sahip olduğundaysa, mevcut kaynak web tarayıcıya geri gönderilir. Web tarayıcı HTTP kaynağını elde eder etmez yeni bir yoklama (polling) mesajı ileterek, süreç yeni bir HTTP kaynağı hazır olana kadar kendisini tekrarlar.
Zaman zaman, sunucu, web tarayıcı veya mevcut ağ ortamında kaynaklı olarak, askıda kalan yoklama isteğinin başarısız olması da söz konusu olabilir. Bu durumda, web tarayıcı hata bilgisini sunucudan alır almaz, yeniden bir istek göndererek, yoklama tekrardan başlar.
LongPolling tekniğinde, web tarayıcı tarafında, bir Javascript nesnesi olan XMLHTTPRequest nesnesi kullanılabilir. Sunucu tarafındaysa, Jax-RS 2 ile birlikte gelecek olan AsyncronousResponse nesneleri esneklik ve kolaylık açısından kullanılabilir.
Şimdi LongPolling tekniği ve Jax-RS 2 birlikteliğini içeren bir Chat uygulaması örneğini sizlerle paylaşmak istiyorum.
Bağımlı olunan kütüphane
Chat uygulaması bir Servlet konteyner üzerinde kolaylıkla çalışabilir, fakat mevcut konteyner ortamının Servlet 3 standardına uyması gerekmektedir. Çünkü Jax-RS 2, konteyner üzerinde Servlet 3 ile birlikte gelen Asenkron işleyicilerini kullanmaktadır.
<dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet</artifactId> <version>2.0-SNAPSHOT</version> </dependency>
Not:
- Jax-RS standardının referans uygulayıcı kütüphanesi Jersey’ dir. http://jersey.java.net/
- Servlet 3 desteği için Jetty 9 veya Tomcat 7 kullanılabilmektedir.
Chat uygulamasının giriş noktası (Rest Endpoint)
@ApplicationPath("app") public class App extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> sets=new HashSet<>(); sets.add(Chat.class); return sets; } }
Yukarıda @ApplicationPath notasyonuyla sarmalanan Application sınıfı türünden nesne, Chat uygulamasının giriş noktasıyın içermektedir. Bir HashSet içine tanımlanan Chat sınıfı ise, Chat uygulamasına ait bir Rest kaynağı olarak tanımlanmaktadır.
Chat sınıfı (Chat Resource)
@Path("/chat") public class Chat { final static Map<String, AsyncResponse> beklemeKuyrugu = new ConcurrentHashMap<>(); final static ExecutorService ex = Executors.newSingleThreadExecutor(); @Path("/{nick}") @GET @Produces("text/plain") public void askidaKal(@Suspended AsyncResponse asyncResp, @PathParam("nick") String nick) { beklemeKuyrugu.put(nick, asyncResp); } @Path("/{nick}") @POST @Produces("text/plain") @Consumes("text/plain") public String mesajGonder(final @PathParam("nick") String nick, final String mesaj) { ex.submit(new Runnable() { @Override public void run() { Set<String> nicks = beklemeKuyrugu.keySet(); for (String n : nicks) { // Mesaj gondericisinden haric herkese gonderilir. if (!n.equalsIgnoreCase(nick)) beklemeKuyrugu.get(n).resume(nick + " dedi ki: " + mesaj); } } }); return "Mesaj gönderildi.."; } }
HTTP/GET metoduyla erişilen askidaKal(..) metodu LongPolling işlemini gören bir RESTful metodudur. Bu metod üzerinde parametre olarak bulunan AsyncResponse türünden parametre, sunucu üzerinde askıda tutulacak LongPolling yanıt nesnesini tanımlamaktadır. AsyncResponse türünden nesneler, Jax-RS ortamından otomatik olarak sağlanırken, duraklatılan (suspended) bu nesneler, geliştirilen mimari çerçevesinde istenildiği zaman yanıt olarak kullanıcıya (web tarayıcıya) sunulabilirler. askidaKal(..) metodunda bulunan ikinci parametre [ @PathParam(“nick“) String nick ] ise, metoda URL üzerinde gönderilen kullanıcı Nick adını elde etmektedir.
Askıda tutulan AsyncResponse nesnelerinin hangi kullanıcıya ait olduğuna sonradan ihtiyaç duyulmaktadır. Bu maksatla, bir Concurrent Map yapısı olan, ConcurrentHashMap bu iş için kullanılabilir. Bu sayede askidaKal(..) metoduna gönderilen her bir nick -> AsyncResponse nesnesi kayıt altında tutulmuş olur.
Chat sınıfının ikinci metodu olan mesajGonder(..) HTTP /POST metoduyla erişilebilen ve Chat mesajlarını web tarayıcıdan elde eden RESTful yordamı olarak iş görmektedir.
Kullanıcılar bir Chat mesajını Nick isim bilgisiyle birlikte sunucuya gönderdiklerinde, mesajGonder(..) RESTful metodu bu bilgileri, metod parametrelerinde elde eder, ve bu bilgiler ExecutorService nesnesi üzerinden Asenkron olarak işletilirler. Burada kullanılan ExecutorService nesnesi yerine elbette basit bir Thread de kullanılabilir.
new Thread(new Runnable(){ … }).start(); gibi. Burada temel mesele, gönderilen mesajın arkaplanda koşulması ve mevcut /POST isteğinin sekteye uğratılmaması, daha doğrusu, mevcut /POST isteğinin tüm kullanıcılara Push edilen mesaj bilgilerinin beklemesini engellemektir.
mesajGonder(..) yordamı içindeki Runnable görev nesnesi içinde, ConcurrentHashMap nesnesi içine bir evvelki askidaKal(..) yordamında doldurulan nick -> AsyncResponse çifti tüketilir, ve kullanıcının kendisi hariç, askıda tutulan tüm AsyncResponse nesnelerine bu Chat mesajı iletilir. AsyncResponse nesnelerinin içinde bulunan resume() metodu, askıda tutulan (@Suspended) yanıt nesnesini, yanıtlamaya olanak tanır. Yani önce duraklat (suspend) ve Chat mesaj kaynağı hazır olduğunda devam ettir (resume) gibi.
Html bileşenleri
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <span></span> <table> <tr> <td>Nick:</td> <td><input id="nick"/></td> </tr> <tr> <td>Mesaj:</td> <td><textarea id="mesaj" disabled="disabled"></textarea></td> </tr> </table> <button>Gönder</button> <ul></ul> <script src="resources/jquery-1.9.1.js" type="text/javascript"></script> <script type="text/javascript" src="resources/chat.js"></script> </body> </html>
Yukarıdaki Html sayfasında, bir Html table nesnesi içine dizelenmiş bir Nick girdi alanı, mesajın girileceği textarea alanı ve mesajın gönderileceği bir Html butonu yer almaktadır. Uygulama ilk açıldığında kullanıcıdan Nick adının girilmesi beklendiği için, textarea alanı başlangıçta veri girilemez (disabled) yapılmıştır. Kullanıcı ilk isteği (Nick adını) gönderdiğinde, bu alan değiştirilemez olmaktan çıkartılarak mesaj yazılabilir hale getirilir. Ayrıca, Nick adı sunucuya gönderildiğinde (yani ilk yoklama isteği yapıldığında) class tipi nick olan ( <tr> ) Html tablo satırı elemanı gizlenir.
Chat uygulamasının XMLHTTPRequest nesneleriyle, HTTP isteklerini asenkron olarak sağlaması gerekmektedir. Biz bu uygulamada bu işlemleri kolaylaştıran ve arka planda XMLHTTPRequest nesnelerini kullanan Jquery Ajax kütüphanesinden faydalanacağız. Jquery bağımlılığına ek olarak, proje içinde bir de, kendi yazdığımız chat.js betiği bulunmaktadır.
chat.js
// Yoklama fonksiyonu bir kereliğine çağrılmalı, // Hata veya başarım durumunda kendisini özyinelemeli olarak çağıracaktır. var yokla = function () { $.ajax({ type: "GET", // HTTP Get isteği url: "/app/chat/" + $("#nick").val(), // askidaKal(..) metoduna erişir dataType: "text", // Gelecek veri tipi text success: function (mesaj) { // Mesaj alındığında, <li/> elemanına mesaj eklenir. $("ul").append("<li>" + mesaj + "</li>"); yokla(); // Bir mesaj tüketildiğinde tekrar yoklamaya bağla. }, error: function () { yokla(); // Bir hata olursa tekrar yoklamaya başla. } }); } // Gönder butonuna tıklandığında; $("button").click(function () { if ($(".nick").css("visibility") === "visible") { // <tr> satırı görünür ise; $("textarea").prop("disabled", false); // veri girilebilir yap $(".nick").css("visibility", "hidden"); // <tr> satırı görünmez yap; $("span").html("Chat başladı.."); // Bilgi mesajı // Yoklama işlemi bir sefer başlatılmalı yokla(); } else // ilk sefer değilse ; $.ajax({ type: "POST", // HTTP POST isteği url: "/app/chat/" + $("#nick").val(), // mesajGonder(..) metoduna erişir. dataType: "text", // Gelecek veri tipi text data: $("textarea").val(), // Gönderilecek Chat mesajı contentType: "text/plain", // Gönderilen Chat mesajının türü success: function (data) { $("span").html(data); // Başarılıysa [Mesaj gönderildi..] yazar. // Blink efekt $("span").fadeOut('fast', function () { $(this).fadeIn('fast'); }); } }); });
Uygulamanın test edilmesi
Uygulamanın pom.xml yapılandırma dosyasında Jetty 9.0.0.RC2 eklentisi yer aldığından, uygulama rahatlıkla ;
> mvn jetty:run-war
komutuyla çalıştırılabilir. Uygulamaya http://localhost:8080/ adresinden erişilebilir.
Ben henüz Maven bilmiyorum diyorsanız Altuğ Bilgin Altıntaş hocamızın aşağıdaki videolarını izleyebilirsiniz.
- https://www.kodedu.com/2011/03/apache-maven-teknolojisine-giris-video/
- https://www.kodedu.com/2011/03/apache-maven-ile-basit-bir-web-uygulamasi-apache-wicket/
Uygulama kodlarına https://github.com/rahmanusta/JaxRS-LongPolling/ üzerinden
Uygulamanın çalışır haline ise http://discoverjavaee.com/JaxRS-LongPolling/ üzerinden erişebilirsiniz.
Tekrar görüşmek dileğiyle..
Tag:jax-rs 2, longpolling
2 Comments