聊天室
聊天室應用程序示例如下:
- 使用channel來實現一個聊天室(pub-sub模式),俗稱的發布-訂閱模式
- 使用Comet和Websockets
應用程序的文件結構如下:
chat/app/ chatroom # Chat room routines chatroom.go controllers app.go # The welcome screen, allowing user to pick a technology refresh.go # Handlers for the "Active Refresh" chat demo longpolling.go # Handlers for the "Long polling" ("Comet") chat demo websocket.go # Handlers for the "Websocket" chat demo views ... # HTML and Javascript
首先我們來看一下這個聊天室是怎么實現的,chatroom.go.
聊天室作為一個獨立的go-routine運行, 如下所示:
func init() {
go chatroom()
}
chatroom() 函數簡單的在3個channel中選擇並執行響應的action
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) ) func chatroom() { archive := list.New() subscribers := list.New() for { select { case ch := <-subscribe: // Add subscriber to list and send back subscriber channel + chat log. case event := <-publish: // Send event to all subscribers and add to chat log. case unsub := <-unsubscribe: // Remove subscriber from subscriber list. } } }
我們來分別看一下每一個都是怎么實現的。
Subscribe
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}
一個訂閱有兩個屬性:
- 聊天日志
- 一個訂閱者能在上面監聽並獲得新信息的channel
Publish
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)
發布的event一個一個發送給訂閱者的channel,然后event被添加到archive,archive里面的數量大於10,前面的會被移出。
Unsubscribe
case unsub := <-unsubscribe: for ch := subscribers.Front(); ch != nil; ch = ch.Next() { if ch.Value.(chan Event) == unsub { subscribers.Remove(ch) } }
訂閱者channel在list中被移除。
Handlers
現在你知道了聊天室是怎么運行的,我們可以看一看handler是怎么使用不同的技術的。
主動刷新
主動刷新聊天室通過javascript每隔5秒刷新頁面來從服務器獲取新信息:
// Scroll the messages panel to the end var scrollDown = function() { $('#thread').scrollTo('max') } // Reload the whole messages panel var refresh = function() { $('#thread').load('/refresh/room?user= #thread .message', function() { scrollDown() }) } // Call refresh every 5 seconds setInterval(refresh, 5000)
以下是請求的action:
func (c Refresh) Room(user string) rev.Result { subscription := chatroom.Subscribe() defer subscription.Cancel() events := subscription.Archive for i, _ := range events { if events[i].User == user { events[i].User = "you" } } return c.Render(user, events) }
它訂閱chatroom並傳遞archive到template來做頁面渲染。這里沒有什么值得看的。
長輪詢(Comet)
長輪詢javascript聊天室使用一個ajax請求server並保持這個連接一直打開知道有一個新消息到來。javascript提供了一個lastReceived時間戳來告訴server,客戶端知道的最新消息是哪個。
var lastReceived = 0 var waitMessages = '/longpolling/room/messages?lastReceived=' var say = '/longpolling/room/messages?user=' $('#send').click(function(e) { var message = $('#message').val() $('#message').val('') $.post(say, {message: message}) }); // Retrieve new messages var getMessages = function() { $.ajax({ url: waitMessages + lastReceived, success: function(events) { $(events).each(function() { display(this) lastReceived = this.Timestamp }) getMessages() }, dataType: 'json' }); } getMessages();
對應的handler
func (c LongPolling) WaitMessages(lastReceived int) rev.Result { subscription := chatroom.Subscribe() defer subscription.Cancel() // See if anything is new in the archive. var events []chatroom.Event for _, event := range subscription.Archive { if event.Timestamp > lastReceived { events = append(events, event) } } // If we found one, grand. if len(events) > 0 { return c.RenderJson(events) } // Else, wait for something new. event := <-subscription.New return c.RenderJson([]chatroom.Event{event}) }
在這種實現里面,它能簡單的阻塞在訂閱channel上(假設它已經發回了所有信息到archive)。
Websocket
Websocket聊天室,當用戶加載了聊天室頁面后,javascript打開了一個websocket連接。
// Create a socket var socket = new WebSocket('ws://127.0.0.1:9000/websocket/room/socket?user=') // Message received on the socket socket.onmessage = function(event) { display(JSON.parse(event.data)) } $('#send').click(function(e) { var message = $('#message').val() $('#message').val('') socket.send(message) });
第一件事是訂閱新的events並加入房間和發出archive,如下所示:
func (c WebSocket) RoomSocket(user string, ws *websocket.Conn) rev.Result { // Join the room. subscription := chatroom.Subscribe() defer subscription.Cancel() chatroom.Join(user) defer chatroom.Leave(user) // Send down the archive. for _, event := range subscription.Archive { if websocket.JSON.Send(ws, &event) != nil { // They disconnected return nil } }
下面我們必須從訂閱監聽新的event, 無論如何websocket庫只提供一個阻塞call來獲得一個新frame,為了在它們之間選擇,我們必須包裝它們。
// In order to select between websocket messages and subscription events, we // need to stuff websocket events into a channel. newMessages := make(chan string) go func() { var msg string for { err := websocket.Message.Receive(ws, &msg) if err != nil { close(newMessages) return } newMessages <- msg } }()
現在我們能在newMessages channel上選擇新的websocket消息。
最后一點就是這樣做的 - 它從websocket等待一個新消息(如果用戶說了什么的話)或從訂閱並傳播消息到其他用戶。
// Now listen for new events from either the websocket or the chatroom. for { select { case event := <-subscription.New: if websocket.JSON.Send(ws, &event) != nil { // They disconnected. return nil } case msg, ok := <-newMessages: // If the channel is closed, they disconnected. if !ok { return nil } // Otherwise, say something. chatroom.Say(user, msg) } } return nil }
如果我們發現websocket channel已經關閉,然后我們返回nil。
至此結束。 ----- 已同步到 一步一步學習Revel Web開源框架