Sunday, June 22, 2014

Aplikasi chat berbasis web menggunakan Socket.io dan NodeJS - Part 2

Pada kesempatan kali ini saya akan melanjutkan pengembangan aplikasi yang tertunda pada Part 1.

Server Logic


Seperti yang sudah saya jelaskan pada postingan sebelumnya, fungsionalitas server terdapat pada file "server.js".

Pada file ini kita akan menambahkan kode untuk menghandle request Socket.io dari client (seperti yang telah di jelaskan sebelumnya Socket.io juga berjalan di client - pada kasus ini web browser)

Integrasikan Socket.io dengan meng-declare nya ke dalam kode sumber:

var express = require('express'), 
    socketIO = require('socket.io'), 
    app = express(), 
    port = 3000 
    ;

Cari baris berikut:

app.listen(port, function(){ 
    console.log('listening on *:' + port); 
});

dan rubah menjadi:

var server = app.listen(port, function(){ 
    console.log('listening on *:' + port); 
}); 

/** 
 * Setup socket.io server untuk meng-handle request 
 * dari library socket.io yang berjalan pada browser 
 */ 
var io = socketIO.listen(server);

Saat ini server Socket.io sudah siap menerima koneksi dari client. Socket.io adalah system yang
berbasis event (event-based) jadi untuk menerima koneksi dari client kita hanya harus membuat
event handler untuk event yang bernama "connection".

io.on('connection', function (socket) {

 // Disini kita akan meng-handle client yang melakukan koneksi 

});

Selanjutnya tambahkan kode yang akan menerima pesan dari client dan mem-broadcast nya
ke semua client:

var users = []; 
var sockets = []; 

io.on('connection', function (socket) {

    socket.on('message', function(message){ 
    
        if (message.type === 'text') {
            console.log('Text Message: ' + message.text); 
            io.emit('message', message); 
        } 

    }); 

});

Jika anda perhatikan saya menggunakan method emit pada object io bukan pada object socket, ini bertujuan untuk mem-broadcast pesan ke semua client tidak hanya pada client yang mengirim pesan saja (sender).

Aplikasi juga harus menyimpan daftar user yang sedang online, dan mem-broadcast nya ke semua client. Untuk itu kita akan merubah sedikit kode di atas menjadi:

io.on('connection', function (socket) {

    socket.on('message', function(message){ 
    
        if (message.type === 'user') {
            console.log('User connected: ' + message.user.name);    
            users.push(message.user); 
            sockets.push(socket); 
            for (var i = 0; i < users.length; ++i) {
                io.emit('connected', users[i]);   
            }               
        } 
        else if (message.type === 'text') {
            console.log('Text Message: ' + message.text); 
            io.emit('message', message); 
        } 

    }); 

});

Ok, Selesai...

Not so fast :), ingat bahwa aplikasi ini terdiri dari dua bagian, kita juga harus menambahkan fungsionalitas dari sisi client.

Client Logic


Pertama-tama kita akan men-setup library Socket.io pada client.
Masukkan (include) library Socket.io pada file "index.html":

<script src="/socket.io/socket.io.js" type="text/javascript"></script>

Dan tambahkan kode berikut di bagian paling bawah body tag (sebelum </body>):

<script src="/socket.io/socket.io.js<script type="text/javascript"> 
        var socket = io(); 
</script>

Yup, hanya ini yang di butuhkan untuk meng-setup library Socket.io di client, sangat mudah bukan!
Anda pasti bertanya-tanya kenapa saya me-refer ke "/socket.io/socket.io.js" pada include tag di atas,
sementara file tersebut tidak dapat di temukan pada root folder dari server. Ini karena Socket.io pada
sisi server akan otomatis membuat file tersebut (generated on-the-fly) saat server berjalan.

Untuk tujuan identifikasi client harus mengirim "username" pada saat pertama kali berjalan.
Tambahkan code berikut pada <script> tag:

var current_user = null;        
var socket = io(); 

function User(name) { 
    this.name = name; 
} 

function createUserMessage(user) { 
    return { type: 'user', user: user };
} 

if (!current_user) { 
    var text = prompt('Your username: ');
    if (!text) { 
        window.location = '/'; 
    } 
    current_user = new User(text); 
} 

// kirim username ke server 
socket.emit('message', createUserMessage(current_user)); 

Kode tersebut akan mengecek apakah user sudah memberikan username, bila belum browser akan
meminta memasukkan terlebih dahulu. Ini akan terus terjadi selama user tidak memberikan
username yang valid (kosong/empty). Jika username yang di berikan adalah valid akan di kirim
ke server.

Jika anda lihat pada file "index.html", terdapat tag yang berfungsi untuk men-display
list dari user yang sedang online:

<div id="side"> 
    <p class="header">Online</p> 
    <ul id="user-list"> 
        <!--<li class="current-user">Fani</li>--> 
    </ul> 
</div>

Ketika ada client yang baru server akan mem-broadcast event "connected", kita dapat men-dengar (listen) event ini dan mem-populate list pada html dengan data yang di return oleh server.

function addToList(id, value, clazz, distinct) {  
    var ul = document.getElementById(id);  
    if (distinct) {  
        if (indexOfList(id, value) > -1) {  
            return;  
        }  
    }  
    var li = document.createElement('li');
    if (clazz) li.className = clazz;  
    li.innerHTML = value;  
    ul.appendChild(li);  
} 

function indexOfList(id, value) { 
    var ul = document.getElementById(id); 
    for (var i = 0; i < ul.children.length; ++i) {
        if (ul.children[i].innerHTML === value) { 
            return i; 
        } 
    } 
    return -1; 
} 

// update list user yang sedang online 
socket.on('connected', function(user) { 
    addToList('user-list', 
              user.name, 
              user.name == current_user.name ? 'current-user' : null, 
              true); 
});

Dan tentunya kita juga harus meng-handle pesan text yang di masukkan oleh user:

function createTextMessage(text, user) { 
    return { type: 'text', text: text, user: user };
} 

// kirim message ke server saat user menekan tombol Enter 
document.forms[0].messageText.onkeypress = function onKeyUp(evt) {
     if (evt.keyCode == 13) {
          socket.emit('message', createTextMessage(this.value, current_user));
          this.value = null;
          evt.preventDefault(); 
      }           
}

Dan men-display pesan text dari semua user:

// handle message yang diterima dari server 
socket.on('message', function(message){ 
          
    if (message.type == 'text') {
        addToList('messages', message.user.name + ': ' + message.text,
            message.user.name === current_user.name ? 'my-message' : null); 
    }   
          
});

Aplikasi sudah fungsional, untuk mencoba jalankan server:

$ node server.js

dan buka alamat berikut di dua browser window/tab: http://localhost:3000

Pada masing-masing window, login dengan dua username yang berbeda, dan mulai chatting.
Anda dapat melihat bahwa saat terdapat perubahan pada halaman di satu browser window, halaman pada browser window yang lain juga mengalami perubahan.

Untuk meng-handle user yang sudah tidak online (meng-hapus nya dari list), saya serahkan kepada anda sebagai bahan pembelajaran. Saya sudah menyiapkan source code nya di sini sebagai bahan referensi.

Sekian, semoga bermanfaat!

Demo

Aplikasi chat berbasis web menggunakan Socket.io dan NodeJS - Part 1

Sebagai konsultan, kami di Nostra seringkali mendapat kesempatan dan di-haruskan bekerja langsung on-site di kantor client dengan memakai PC yang telah di-sediakan dan biasanya akses administratif serta jaringannya di lock-down sesuai dengan policy perusahaan tersebut.

Sering kali komunikasi antar team member menjadi terhambat apalagi kalau meja-nya saling berjauhan, karena tidak mungkin saling bersautan dengan nada suara tinggi atau menghampiri kesana kemari hanya untuk hal kecil yang tidak membutuhkan lisan (misal: Technical Leader menginformasikan kesalahan kecil yg terdapat dalam aplikasi yang sedang dikerjakan, sehingga programmer dapat langsung dapat memperbaikinya). Masalah ini dapat di tanggulangi dengan memakai IM seperti Skype, tetapi biasanya tidak memungkinkan karena policy dari perusahan client.

Hal inilah yang menginspirasi saya untuk membuat aplikasi chat sederhana berbasis web (Ya..., akses ke internet melalui web browser pada umumnya tidak di anggap "membahayakan" secara internal sehingga di perbolehkan) yang nantinya dapat berguna bagi kami yang sedang bekerja di kantor client dan mengalami masalah serupa.

Technical Feasibility


Untuk membuat aplikasi ini kita membutuhkan suatu teknologi yang memungkinkan komunikasi dua arah (full-duplex) antara web browser dan server sehingga data di web browser akan terupdate secara real-time apabila data yang di server mengalami perubahan. Seperti yang sudah kita ketahui aplikasi web dibangun di atas protokol HTTP di mana protokol ini tidak men-support komunikasi dua arah (biderectional communication) antara client (web browser) dan server. Untuk mengakali keterbatasan protokol HTTP ini, ada dua teknik yang pada saat ini di pakai, yaitu:

  1. Periodic Polling, yaitu teknik di mana si Client melakukan request secara berkala ke Server untuk menge-cek apakah ada data yang baru dan kemudian melakukan update pada data yang sedang display. 
  2. Long-polling, yaitu teknik dimana si Client melakukan request untuk menge-cek apakah ada data yang baru namun si Server tidak langsung me-respon tetapi akan menjaga (hold) koneksi tetap terbuka selama tidak terdapat data yang baru. Teknik long-polling inilah yang saat di pakai untuk membuat aplikasi chat berbasis web seperti Gmail chat, Facebook chat, dll.

Sebagai alternatif dari dua teknik tradisional di atas, kita dapat menggunakan fitur HTML5 terbaru yaitu WebSocket, tetapi karena standar HTML5 relatif masih baru, hanya beberapa browser versi terbaru yang men-support WebSocket sehingga kurang fleksibel.

Socket.io


Socket.io adalah library JavaScript yang memungkinkan komunikasi real-time antara web browser dan server. Library ini terdiri dari dua bagian, yang berjalan pada web browser dan yang berjalan pada server sebagai NodeJS module. Socket.io akan menggunakan WebSocket sebagai sarana tansport utama dan akan meng-fallback ke beberapa teknik transport lainnya apabila WebSocket tidak tersedia, seperti Flash (menggunakan socket), Ajax long-pooling, atau iframe, sehingga tetap dapat berjalan walaupun browser tidak meng-support WebSocket. Socket.io juga memiliki fitur yang sangat berguna yaitu dengan sendirinya akan mencoba membuat ulang koneksi (reconnect) apabila koneksi terputus.

Environment Setup


Karena Socket.io berjalan di atas NodeJS sebagai module, kita harus terlebih dahulu meng-install NodeJS. Jika anda memakai Windows atau Mac bisa langsung ke http://nodejs.org/download untuk men-download installer nya. Karena saya memakai ArchLinux jadi saya akan menginstall lewat pacman:

$ pacman -S nodejs

(untuk distro linux lainnya bisa mengikuti langkah-langkah yang di berikan disini)

Setelah NodeJS terinstall, lanjutkan dengan meng-install module-module yang di perlukan yaitu:
- Socket.io : library utama yg akan di gunakan
- ExpressJS : http server framework untuk NodeJS (ini akan digunakan untuk melayani file html ke browser)

Untuk meng-install nya kita akan menggunakan Node Package Manager (npm) yang sudah tersedia pada saat kita menginstall NodeJS.

Agar aplikasi yang akan di buat lebih terstruktur, semua file-file yang bersangkutan akan di letakkan pada satu tempat.
Buat folder NostraChat. Folder ini akan digunakan sebagai project root.

$ mkdir NostraChat
$ cd NostraChat

Buat file package.json pada folder tersebut dengan isi sebagai berikut:

{
    "name": "NostraChat",
    "version": "1.0.0",
    "description": "Simple Chat Web App",
    "dependencies": {
        "socket.io": "latest",
        "express": "latest"
    },
    "author": "nostra-dev"
}

Lalu di console jalankan perintah:

$ npm install

Ini akan membutuhkan beberapa waktu karena npm harus men-download module-module tersebut dari internet. Saat npm selesai, module-module tersebut akan ter-install di ./node_modules

Struktur Aplikasi


Aplikasi chat yang akan kita kembangkan ini terdiri dari server (backend) dan sebuah file html sebagai halaman utama (front-end).

Langkah pertama adalah menyiapkan server untuk me-respon dengan halaman utama yang berupa
file html saat mendapatkan request dari web browser. Buat sub-folder "public", dan di dalam folder ini buat file bernama "index.html" dengan isi sebagai berikut:

<!DOCTYPE html> 
<html> 
<head> 
    <title>Nostra Chat</title> 
    <style type="text/css"> 
        body, div, p { margin: 0; padding: 0; } 
        body { font-family: "Arial"; } 
        form { position: fixed; bottom: 0; width: 100%; margin: 0; padding: 0; } 
        form textarea { width: 100%; height: 50px; padding: 10px; margin: 0; border: 1px solid #bbb; }
        #messages { list-style-type: none; margin: 0; padding: 0; background: #fff; } 
        #messages li { padding: 5px 10px; }         
        #main { 
            width: 85%; 
            margin: 10px; 
        } 
        #side { 
            width: 15%; 
            background: #fff; 
            position: absolute; 
            top: 0; bottom: 75px; right: 0; 
            border: 1px solid #bbb; 
        } 
        #side .header { 
            background: #eee; 
            color: #333; 
            margin: 0; 
            padding: 5px 10px; 
            font-weight: bold; 
            position: relative; 
            border-bottom: 1px solid #bbb; 
        } 
        #side .header p { 
            width: 100%; 
        } 
        #side .header #logout { 
            text-align: right; 
            position: absolute; 
            right: 10px; 
            text-decoration: none; 
            color: #888; 
        } 
        #side .header #logout:hover { 
            color: #333; 
        } 
        #user-list { 
            list-style-type: none; margin: 0; padding: 0; 
            padding: 5px 10px; 
        } 
        #user-list .current-user { font-weight: bold; } 
    </style> 
</head> 

<body> 
    <div id="main"> 
        <ul id="messages"> 
            <!--<li class="my-message">Hi server</li>--> 
            <!--<li>Hello client</li>--> 
        </ul> 
    </div> 
     
    <div id="side"> 
        <p class="header">Online</p> 
        <ul id="user-list"> 
            <!--<li class="current-user">Fani</li>--> 
        </ul> 
    </div> 
     
    <form action=""> 
      <textarea name="messageText" autocomplete="off"></textarea> 
    </form>       
</body> 
</html>

Kode untuk men-setup server di letakkan pada file bernama "server.js" di project root (NostraChat).
File ini akan di jalankan (execute) oleh NodeJS.

Adapun isi dari file server.js tersebut adalah sebagai berikut, kodenya sudah saya berikan comment, jadi saya rasa sudah menjelaskan dengan sendiri-nya :)

var express = require('express'), 
    app = express(), 
    port = 3000 
    ; 

/** 
 * Meng-konfigurasi express untuk meng-look-up file html 
 * dari directory bernama "public" 
 */ 
app.use(express.static(__dirname + '/public')); 

/** 
 * Pada saat browser me-request path "/" 
 * response dengan file bernama "index.html" 
 */ 
app.get('/', function(req, res){
    res.sendfile('index.html'); 
}); 

/* 
 * Meng-konfigurasi express untuk listen pada 
 * port yang telah di tentukan 
 */ 
app.listen(port, function(){ 
    console.log('listening on *:' + port); 
});

Jalankan server dengan:

$ node server.js

Lalu buka di browser: http://localhost:3000

So far so good, halaman utama aplikasi sudah berjalan tetapi belum fungsional, di sinilah saatnya kita meng-integrasikan Socket.io pada aplikasi agar menjadi fungsional.

Stay tuned for Part 2...

Monday, June 16, 2014

Hibernate OGM

Hibernate OGM (Object Grid Mapper) merupakan persistence framework untuk NoSQL datastore (seperti MongoDB). Berbeda dengan Hibernate ORM yang khusus untuk relational database seperti MySQL, Oracle, postgresql dll.
Sayang sekali versi yang terakhir direlease masih Beta (belum final atau stable), yakni 4.1.0.Beta4. Jadi kemungkinan masih banyak kekurangan dan perlu banyak pengembangan.
Jika kita sudah familiar dengan JPA tentunya akan lebih mudah untuk menggunakan framework ini.
Untuk dokumentasi lebih lengkapnya bisa dilihat disini.

Sekarang kita mulai saja dengan contohnya.

Database yang akan digunakan adalah MongoDB.
Start mongoDB admin dan client nya
 $ mongod  
 $ mongo  

Create java project dengan struktur maven. Tambahkan library MongoDB driver dan MongoDB hibernate OGM di pom.xml
 <dependency>  
       <groupId>org.mongodb</groupId>  
       <artifactId>mongo-java-driver</artifactId>  
       <version>2.12.2</version>  
 </dependency>  
 <dependency>  
       <groupId>org.hibernate.ogm</groupId>  
       <artifactId>hibernate-ogm-mongodb</artifactId>  
       <version>4.1.0.Beta3</version>  
     </dependency>  


Tambahkan library core untuk Hibernate OGM
 <dependency>  
       <groupId>org.hibernate.ogm</groupId>  
       <artifactId>hibernate-ogm-core</artifactId>  
       <version>4.1.0.Beta4</version>  
     </dependency>  
     <dependency>  
       <groupId>org.hibernate.ogm</groupId>  
       <artifactId>hibernate-ogm-infinispan</artifactId>  
       <version>4.1.0.Beta4</version>  
     </dependency>  

Create satu contoh domain entity class. Misalnya class Employee. Hampir sama dengan hibernate ORM, karena kita memakai standart JPA.

 @Entity  
 public class Employee {  
   @Id  
   @GeneratedValue(strategy = GenerationType.AUTO)  
   private Integer id;  
   @Column(name = "firstname")  
   private String firstname;  
   @Column(name = "lastname")  
   private String lastname;  
   @Column(name = "phone")  
   private String phone;  
   ……       
 }  


Create persistence.xml di resource/META-INF yang berisi daftar class mapping dan koneksi ke datastrore.

Jangan lupa providernya diset  “org.hibernate.ogm.jpa.HibernateOgmPersistence”.
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>  
 <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">  
 <persistence-unit name="samplePersistenceUnit" transaction-type="RESOURCE_LOCAL">  
   <!-- Use Hibernate OGM provider: configuration will be transparent -->  
   <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>  
   <class>com.andri.hibernate.sample.persistence.domain.Employee</class>  
   <class>com.andri.hibernate.sample.persistence.domain.Address</class>  
   <properties>  
       <!-- Configure Hibernate OGM to mount Infinispan -->  
       <property name="hibernate.ogm.datastore.provider" value="org.hibernate.ogm.datastore.mongodb.impl.MongoDBDatastoreProvider"/>  
       <property name="hibernate.ogm.datastore.database" value="sample-mongo-test"/>  
       <property name="hibernate.ogm.datastore.host" value="localhost"/>  
       <property name="hibernate.search.default.directory_provider" value="filesystem"/>  
       <property name="hibernate.search.default.indexBase" value="/tmp/.hibernate_ogm_demo_luceneindexes"/>  
       <property name="hibernate.ogm.datastore.grid_dialect" value="org.hibernate.ogm.datastore.mongodb.MongoDBDialect" />  
   </properties>  
   </persistence-unit>  
 </persistence>  

Create sample class DAO yang mengimplementasikan proses CRUD untuk domain yang sudah kita buat tadi (persist, merge, delete).
 public class EmployeeDao {  
   private EntityManagerFactory emf = Persistence.createEntityManagerFactory("samplePersistenceUnit");  
   private EntityManager em = emf.createEntityManager();  
   public void save(Employee employee){  
     EntityTransaction transaction = em.getTransaction();  
     try {  
       transaction.begin();  
       em.persist(employee);  
       transaction.commit();  
     }catch (Exception e){  
       if(transaction !=null && transaction.isActive())  
         transaction.rollback();  
     }  
   }  
   public void update(Employee employee){  
     EntityTransaction transaction = em.getTransaction();  
     try {  
       if(findById(employee.getId())!=null) {  
         transaction.begin();  
         em.merge(employee);  
         transaction.commit();  
       }  
     }catch (Exception e){  
       if(transaction !=null && transaction.isActive())  
         transaction.rollback();  
     }  
   }  
   public void delete(Employee employee){  
     EntityTransaction transaction = em.getTransaction();  
     try {  
       if(findById(employee.getId())!=null) {  
         transaction.begin();  
         em.remove(employee);  
         transaction.commit();  
       }  
     }catch (Exception e){  
       if(transaction !=null && transaction.isActive())  
         transaction.rollback();  
     }  
   }  
   public void deleteById(Integer id){  
     EntityTransaction transaction = em.getTransaction();  
     try {  
       Employee find = findById(id);  
       if(find != null) {  
         transaction.begin();  
         em.remove(find);  
         transaction.commit();  
       }  
     }catch (Exception e){  
       if(transaction !=null && transaction.isActive())  
         transaction.rollback();  
     }  
   }  
   public Employee findById(Integer id) {  
     try {  
       return em.find(Employee.class, id);  
     } catch (Exception ex) {  
       System.out.println("Could not find Employee with id: " + id + "; " + ex.getMessage());  
     }  
     return null;  
   }  
   public EntityManager getEntityManager() {  
     return em;  
   }  
 }  

Create Test class untuk mencoba proses CRUD di dao.
 public class EmployeeDaoTest {  
   private EmployeeDao employeeDao = new EmployeeDao();  
   @Test  
   public void testInsert(){  
     Employee employeeFirst = new Employee();  
     employeeFirst.setFirstname("Andri");  
     employeeFirst.setLastname("Sasuke");  
     employeeFirst.setPhone("0817777");  
     Employee employeeSecond = new Employee();  
     employeeSecond.setFirstname("Budi");  
     employeeSecond.setLastname("Ini");  
     employeeSecond.setPhone("081888");  
     employeeDao.save(employeeFirst);  
     employeeDao.save(employeeSecond);  
     Assert.assertTrue(employeeFirst.getId()!=null && employeeSecond.getId()!=null);  
   }  
   ………..  
 }  

Masuk mongoDB client,  dapat dilihat data yang kita insert masuk.
Dengan perintah : $ db.Employee.find()


Data formatnya JSON. Kalau di MongoDB table dianggap menjadi sebuah Collection da nisi datanya formatnya JSON.
Untuk spesifikasi lebih lanjut tentang MongoDB bisa diliat di sini.

Untuk proses select query saya masih belum menemukan caranya. Jika digunakan query JP-QL ternyata masih error. Kalo dibaca baca di documentasinya mungkin bisa menggunakan Hibernate Search.

Source code tutorial ini bisa dilihat di sini : https://github.com/andrisasuke/sample-hibernate-ogm-mongoDB

Mungkin kedepannya tutorial ini bisa dilengkapi dengan Spring atau Spring MVC

Selamat mencoba dan maaf atas kekurangannya. J