[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 ...
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