[revel framework] websocket qua simple app demo (phần 1)

Trước revel mình chưa hề làm việc với websoket và khái niệm về nó mình cũng chỉ biết qua qua thôi. Nhưng trong report tuần này mình viết về nó(websocket) lại còn trên 1 framework cực kì lạ lẫm. Không phải vì mình giỏi đâu các bạn ạ, mà là vì Websocket đã được hỗ trợ tối đa trong framework này(revel). OK Chúng ta cùng tìm hiểu về nó nhé.

1. Giới thiệu

Về khoản websocket là j mình xin phép được bỏ qua vì nó không phải nội dung trọng tâm của bài viết này. Bài viết này mình chỉ muốn chỉ ra sự thú vị của websocket trong revel framework. Nó đã được hỗ trợ như là 1 kiểu action trong controller của các fw thông thường(PUT, PATCH, GET...) đó là kiểu WS.

2. Tạo simple app chat

đầu tiên bạn phải tạo router cho application ở conf/routes

GET     /websocket/room                         WebSocket.Room
WS      /websocket/room/socket                  WebSocket.RoomSocket

đây là file router của revel syntax của khai báo route là:

[METHOD] [URL Pattern] [Controller.Action]

OK vậy là đã có url cho app, h chúng ta sẽ tạp controller cho app.

 //app/controlller/websocket.go
package controllers

import (
	// core of revel framework
	"github.com/revel/revel"
)

type WebSocket struct {
	*revel.Controller
}

func (c WebSocket) Room() revel.Result {

}

func (c WebSocket) RoomSocket() revel.Result {

}

trong hàm get khi vào url chúng ta sẽ truyền vào 1 param làm tên cho người dùng trong method Room. và render nó ra view.

 func (c WebSocket) Room(user string) revel.Result {
	return c.Render(user)
}

còn đây là view cho ứng dụng:

//app/view/header.html
<!DOCTYPE html>

<html>
  <head>
    <title>{{.title}}</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <link rel="stylesheet" type="text/css" media="screen" href="/public/stylesheets/main.css">
    <link rel="shortcut icon" type="image/png" href="/public/images/favicon.png">
    <script src="/public/javascripts/jquery-1.5.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="/public/javascripts/templating.js" type="text/javascript" charset="utf-8"></script>
    <script src="/public/javascripts/jquery.scrollTo-min.js" type="text/javascript" charset="utf-8"></script>
  </head>
  <body>
//app/view/websocket/Room.html
{{set . "title" "Chat room"}}
{{template "header.html" .}}

<h1>WebSocket — You are now chatting as {{.user}}
  <a href="/">Leave the chat room</a></h1>

<div id="thread">
</div>

<div id="newMessage">
  <input type="text" id="message" autocomplete="off" autofocus>
  <input type="submit" value="send" id="send">
</div>

cú pháp ngoài trong view template của revel giống hệt với blade template trong laravel fw nên cũng không quá khó để hiểu về nó. h mình sẽ sẽ sang phần chính đó là xây dựng chat realtime. Trước tiên chúng ta thiết lập connect cho phía client.

//app/view/websocket/Room.html
{{set . "title" "Chat room"}}
{{template "header.html" .}}

<h1>WebSocket — You are now chatting as {{.user}}
  <a href="/">Leave the chat room</a></h1>

<div id="thread">
  <script type="text/html" id="message_tmpl">
    <% if(event.Type == 'message') { %>
      <div class="message <%= event.User == '{{.user}}' ? 'you' : '' %>">
        <h2><%= event.User %></h2>
        <p>
          <%= event.Text %>
        </p>
      </div>
    <% } %>
    <% if(event.Type == 'join') { %>
      <div class="message notice">
        <h2></h2>
        <p>
          <%= event.User %> joined the room
        </p>
      </div>
    <% } %>
    <% if(event.Type == 'leave') { %>
      <div class="message notice">
        <h2></h2>
        <p>
          <%= event.User %> left the room
        </p>
      </div>
    <% } %>
    <% if(event.Type == 'quit') { %>
      <div class="message important">
        <h2></h2>
        <p>
          You are now disconnected!
        </p>
      </div>
    <% } %>
  </script>
</div>

<div id="newMessage">
  <input type="text" id="message" autocomplete="off" autofocus>
  <input type="submit" value="send" id="send">
</div>

<script type="text/javascript">

  // Create a socket
  var socket = new WebSocket('ws://'+window.location.host+'/websocket/room/socket?user={{.user}}')

  // Display a message
  var display = function(event) {
    $('#thread').append(tmpl('message_tmpl', {event: event}));
    $('#thread').scrollTo('max')
  }

  // Message received on the socket
  socket.onmessage = function(event) {
    console.log(event)
    display(JSON.parse(event.data))
  }

  $('#send').click(function(e) {
    var message = $('#message').val()
    $('#message').val('')
    socket.send(message)
  });

  $('#message').keypress(function(e) {
    if(e.charCode == 13 || e.keyCode == 13) {
      $('#send').click()
      e.preventDefault()
    }
  })

</script>

còn bên phía server. Trước tiên ta phải include thêm package websocket thư viện do revel hỗ trợ. và 1 thư viện hỗ trợ kết nối connect(mình sẽ có bài mổ sẻ thư viện này sau).

//app/controllers/websocket.go
import (
	//thu vien de van hanh websocket
	"golang.org/x/net/websocket"
	//thu vien nhan class revel
	"github.com/revel/revel"
	// thu vien chat do user viet
	"/app/tuanna"
)
//app/tuanna/tuanna.go
package tuanna

import (
	"container/list"
	"time"
)

type Event struct {
	Type      string // "join", "leave", or "message"
	User      string
	Timestamp int    // Unix timestamp (secs)
	Text      string // What the user said (if Type == "message")
}

type Subscription struct {
	Archive []Event      // All the events from the archive.
	New     <-chan Event // New events coming in.
}

// Owner of a subscription must cancel it when they stop listening to events.
func (s Subscription) Cancel() {
	unsubscribe <- s.New // Unsubscribe the channel.
	drain(s.New)         // Drain it, just in case there was a pending publish.
}

func newEvent(typ, user, msg string) Event {
	return Event{typ, user, int(time.Now().Unix()), msg}
}

func Subscribe() Subscription {
	resp := make(chan Subscription)
	subscribe <- resp
	return <-resp
}

func Join(user string) {
	publish <- newEvent("join", user, "")
}

func Say(user, message string) {
	publish <- newEvent("message", user, message)
}

func Leave(user string) {
	publish <- newEvent("leave", user, "")
}

const archiveSize = 10

var (
	// Send a channel here to get room events back.  It will send the entire
	// archive initially, and then new messages as they come in.
	subscribe = make(chan (chan<- Subscription), 10)
	// Send a channel here to unsubscribe.
	unsubscribe = make(chan (<-chan Event), 10)
	// Send events here to publish them.
	publish = make(chan Event, 10)
)

// This function loops forever, handling the chat room pubsub
func chatroom() {
	archive := list.New()
	subscribers := list.New()

	for {
		select {
		case ch := <-subscribe:
			var events []Event
			for e := archive.Front(); e != nil; e = e.Next() {
				events = append(events, e.Value.(Event))
			}
			subscriber := make(chan Event, 10)
			subscribers.PushBack(subscriber)
			ch <- Subscription{events, subscriber}

		case event := <-publish:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				ch.Value.(chan Event) <- event
			}
			if archive.Len() >= archiveSize {
				archive.Remove(archive.Front())
			}
			archive.PushBack(event)

		case unsub := <-unsubscribe:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				if ch.Value.(chan Event) == unsub {
					subscribers.Remove(ch)
					break
				}
			}
		}
	}
}

func init() {
	go chatroom()
}

// Helpers

// Drains a given channel of any messages.
func drain(ch <-chan Event) {
	for {
		select {
		case _, ok := <-ch:
			if !ok {
				return
			}
		default:
			return
		}
	}
}

Việc phân tích thư viện này mình xin giành cho bài viết sau. H chúng ta sẽ chỉ include vào để sử dụng. Một điều rất hay là revel fw áp dụng theo mô hình dependence injection nên các phần rất tách bạch với nhau(tiện cho những người lười như mình 😃 ). h chúng ta chỉ việc lôi các hàm ra và sử dụng trong controller xử lí logic websocket.

// This function loops forever, handling the chat room pubsub
func chatroom() {
	archive := list.New()
	subscribers := list.New()

	for {
		select {
		case ch := <-subscribe:
			var events []Event
			for e := archive.Front(); e != nil; e = e.Next() {
				events = append(events, e.Value.(Event))
			}
			subscriber := make(chan Event, 10)
			subscribers.PushBack(subscriber)
			ch <- Subscription{events, subscriber}

		case event := <-publish:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				ch.Value.(chan Event) <- event
			}
			if archive.Len() >= archiveSize {
				archive.Remove(archive.Front())
			}
			archive.PushBack(event)

		case unsub := <-unsubscribe:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				if ch.Value.(chan Event) == unsub {
					subscribers.Remove(ch)
					break
				}
			}
		}
	}
}

Tổng kết

Bản thân mình thấy Golang vẫn còn khá là thô so với các framework phổ biến. nhưng nó cũng có 1 điểm hay đó là hỗ trợ cho websocket gần như tối đa. coi nó như là 1 route thông thường. P/s: bài viết này lấy ví dụ từ trang chủ revel, mình đọc code và trình bày lại theo ý hiểu của mình, bạn muốn nhìn tổng quát hơn có thể lên trang chủ để đọc. Bài viết lần sau mình sẽ đi vào mổ sẻ thư viện chat room của revel.

Tài liệu liên quan

https://viblo.asia/tuanna2704/posts/xQMGJmMXvam