12/08/2018, 13:28

Server Send Event

As for web application grew over the past few years, the need for real time data update has been increased. Web apps we use every day rely on real-time features—the sort of features that let you see new posts magically appearing at the top of your feeds without having to lift a finger. Polling ...

As for web application grew over the past few years, the need for real time data update has been increased. Web apps we use every day rely on real-time features—the sort of features that let you see new posts magically appearing at the top of your feeds without having to lift a finger.

Polling is a traditional technique used by the vast majority of AJAX applications. The basic idea is that the application repeatedly polls a server for data. If you're familiar with the HTTP protocol, you know that fetching data revolves around a request/response format. The client makes a request and waits for the server to respond with data. If none is available, an empty response is returned. So what's the big deal with polling? Extra polling creates greater HTTP overhead. WebSockets is another technology, it built on top of TCP protocol. It hold the connection to the server open so that the server can send information to the client, even in the absence of a request from the client. WebSockets allow for bi-directional, "full-duplex" communication between the client and the server by creating a persistent connection between the two. Having a two-way channel is more attractive for things like games, messaging apps, and for cases where you need near real-time updates in both directions. However, in some scenarios data doesn't need to be sent from the client. You simply need updates from some server action that is where Server Send Event comes in. SSEs are sent over traditional HTTP. That means they do not require a special protocol or server implementation to get working.

We are going to implement a simple chat application using SSEs which make use of Rails ActionController::Live module to stream data and Redis PubSub to publish data when user send new message.

Server Side

class ChatsController < ApplicationController
  include ActionController::Live

  def index
  end

  def new
    response.headers['Content-Type'] = 'text/event-stream'
    client = Redis.new
    sse = SSE.new(response.stream)
    client.subscribe 'chat' do |on|
      on.message do |channel, message|
        sse.write(message, event: 'message')
      end
    end
  rescue IOError
    Rails.logger.info('Connection close')
  ensure
    sse.close
    client.quit
  end

  def create
    client = Redis.new
    client.publish('chat', { user: params[:user], message: params[:message]}.to_json)
    client.quit
    render nothing: true, status: 200
  end
end

To make this controller streamable we need to include ActionController::Live and set response header to content type to text/event-stream. We can now use stream object to send out data to client. In order for client to receive updated message we subcribe each client to chat channel and when the new message comes in we publish the message to this channel. SSE is just a wrap around stream object here is its source code

class SSE
  def initialize(io)
    @io = io
  end

  def write(message, options = {})
    options.each do |k, v|
      @io.write("#{k}: #{v}
")
    end
    @io.write("data: #{message}

")
  end

  def close
    @io.close
  end
end

Messages are delimited by two newlines. The data field is the event’s payload. Next, update your routes.rb to point at the new controller

Rails.application.routes.draw do
  resources :chats
end

Client Side

$(document).on 'ready page:load', ->
  name = '
  chatbox = $('#chatbox')
  source = new EventSource('/chats/new')
  source.addEventListener 'message', (e) ->
    appendMessage(e.data)

  $('#chat').hide();
  $('#btn-register').on 'click', (e) ->
    e.preventDefault()
    name = $('#name').val()
    $('#register').hide()
    $('#chat').show();

  $('#btn').on 'click', (e) ->
    e.preventDefault()
    textBox = $('#message')
    message = textBox.val()
    textBox.val(')
    $.ajax({
      url: '/chats',
      method: 'POST',
      data: { user: name, message: message }
    })

  appendMessage = (data) ->
    object = JSON.parse(data)
    node = document.createElement('div')
    node.className = 'chat-message'
    node.innerHTML = "#{object.user}: #{object.message}"
    chatbox.append(node)

The point to note here is we create a new EventSource object and passed it a url to ChatsController#new, this result in subscribe client to receive data from server. Next we listen to message event and append new message into DOM. We make use of AJAX to post data to ChatsController#create which in turn publish through redis to chat channel and as a result push new message to all clients that subscribe to that channel.

Webservers

By default, rails server uses WEBrick. The Rack adapter for WEBrick buffers all output in a way we cannot bypass, so developing this example with rails server will not work. We will use Puma an opensource concurrent webserver for this project. Drop in the puma gem and start server with command rails s puma

gem 'puma'

Now open your browser and visit http://localhost:3000/chats.

0