Revel示例 - 聊天室


聊天室


聊天室應用程序示例如下:

  • 使用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

Browse the code on Github

首先我們來看一下這個聊天室是怎么實現的,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)

Refresh/Room.html

以下是請求的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)
}

refresh.go

它訂閱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();

LongPolling/Room.html

對應的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})
}

longpolling.go

在這種實現里面,它能簡單的阻塞在訂閱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)
  });

WebSocket/Room.html

第一件事是訂閱新的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
        }
    }

websocket.go

下面我們必須從訂閱監聽新的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
        }
    }()

websocket.go

現在我們能在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.go

如果我們發現websocket channel已經關閉,然后我們返回nil。

 

至此結束。 ----- 已同步到 一步一步學習Revel Web開源框架


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM