Author : Ali0thjavascript
Date : 2019-4-26css
go get github.com/astaxie/beego # beego
go install # 在beego目錄下安裝
go get github.com/beego/bee # 工具
bee version # 執行成功說明安裝成功
複製代碼
├── conf
│ └── app.conf
├── controllers
│ ├── admin
│ └── default.go
├── main.go
├── models
│ └── models.go
├── static
│ ├── css
│ ├── ico
│ ├── img
│ └── js
└── views
├── admin
└── index.tpl
複製代碼
下面咱們從示例代碼 在線聊天室 WebIM
來學習 beego 吧。html
go get github.com/beego/samples/tree/master/WebIM
cd $GOPATH/src/github.com/beego/samples/WebIM
go get github.com/gorilla/websocket
go get github.com/beego/i18n
bee run # 啓動
複製代碼
問題與修復:java
新版本 beego 沒有 beego.Info 、 beego.Error 、logs.Trace 等。git
修復:github
新版本 beego 把其引用方式修改了,放在了 logs 裏。只要把代碼修改爲 logs.Info 、logs.Error 便可。golang
這裏使用 bash 腳本全局替換:web
# 所有進行替換
sed -i "s/beego.Info/logs.Info/g" `grep beego.Info -rl --include="*.go" ./`
sed -i "s/beego.Error/logs.Error/g" `grep beego.Error -rl --include="*.go" ./`
sed -i "s/beego.Trace/logs.Trace/g" `grep beego.Trace -rl --include="*.go" ./`
複製代碼
websocket 比較好,這裏就主要分析websocket方式。啓動服務後,選擇 websocket 技術。能夠看到發起的請求創建 websocket 鏈接。chrome
都是服務器推送技術(Push technology)中的一種。json
長輪詢模式是一種 HTTP 短鏈接。長輪詢意味着瀏覽器只需啓動一個HTTP請求,其鏈接的服務器會「hold」住這次鏈接,直到有新消息才返回響應信息並關閉鏈接,客戶端處理完響應信息後再向服務器發送新的Http請求,以此類推。
WebSocket是一種在單個TCP鏈接上進行全雙工通訊的協議。WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。
爲何 websocket 更好? HTTP 是無狀態協議,HTTP通訊只能由客戶端發起,HTTP請求可能須要在每一個請求都攜帶狀態信息(如身份認證等)。websocket 是有狀態協議,通訊能夠省略部分狀態信息。
總體使用 MVC 模式,架構比較簡單,這裏對主要對關鍵部分代碼作註釋說明。
WebIM/
WebIM.go # main 包的文件
conf
app.conf # 配置文件
controllers
app.go # 供用戶選擇技術和用戶名的歡迎頁面
chatroom.go # 數據管理相關的函數
longpolling.go # 長輪詢模式的控制器和方法
websocket.go # WebSocket 模式的控制器和方法
models
archive.go # 操做數據相關的函數
views
... # 模板文件
static
... # JavaScript 和 CSS 文件
複製代碼
routers/router.go
package routers
import (
"github.com/beego/samples/WebIM/controllers"
"github.com/astaxie/beego"
)
func init() {
// Register routers.
beego.Router("/", &controllers.AppController{}) // 首頁
// Indicate AppController.Join method to handle POST requests.
beego.Router("/join", &controllers.AppController{}, "post:Join") // 加入
// Long polling.
beego.Router("/lp", &controllers.LongPollingController{}, "get:Join")
beego.Router("/lp/post", &controllers.LongPollingController{})
beego.Router("/lp/fetch", &controllers.LongPollingController{}, "get:Fetch")
// WebSocket.
beego.Router("/ws", &controllers.WebSocketController{}) //
beego.Router("/ws/join", &controllers.WebSocketController{}, "get:Join") // 加入
}
複製代碼
package controllers
import (
"container/list"
"github.com/astaxie/beego/logs"
"time"
"github.com/beego/samples/WebIM/models"
"github.com/gorilla/websocket"
)
type Subscription struct { // 訂閱
Archive []models.Event // All the events from the archive.
New <-chan models.Event // New events coming in.
}
func newEvent(ep models.EventType, user, msg string) models.Event { // 新事件傳入,返回成事件結構體
return models.Event{ep, user, int(time.Now().Unix()), msg}
}
func Join(user string, ws *websocket.Conn) { // 將訂閱者加入通道 subscribe 中
subscribe <- Subscriber{Name: user, Conn: ws}
}
func Leave(user string) { // 將用戶加入到不訂閱通道中
unsubscribe <- user
}
type Subscriber struct { // 定義'訂閱者'結構體
Name string
Conn *websocket.Conn // Only for WebSocket users; otherwise nil.
}
var ( // 定義三個有緩衝的通道和兩個列表,通道分別爲 訂閱、未訂閱、發佈,列表分別爲等待列表、訂閱者列表。
// Channel for new join users.
subscribe = make(chan Subscriber, 10) // 建立有緩衝的通道,buffer 爲 10,通道中數據格式爲 Subscriber 結構體
// Channel for exit users.
unsubscribe = make(chan string, 10)
// Send events here to publish them.
publish = make(chan models.Event, 10)
// Long polling waiting list.
waitingList = list.New() // 長輪詢的等待隊列
subscribers = list.New() // 建立訂閱者列表
)
// This function handles all incoming chan messages.
func chatroom() { // 聊天室
for {
select {
case sub := <-subscribe: // 檢查是否有新的訂閱者,當有新訂閱者時會觸發此 case
if !isUserExist(subscribers, sub.Name) { // 檢查是不是已經訂閱的用戶
subscribers.PushBack(sub) // Add user to the end of list.
// Publish a JOIN event.
publish <- newEvent(models.EVENT_JOIN, sub.Name, "") // 將 EVENT_JOIN 事件加入到 publish 通道中
logs.Info("New user:", sub.Name, ";WebSocket:", sub.Conn != nil)
} else {
logs.Info("Old user:", sub.Name, ";WebSocket:", sub.Conn != nil)
}
case event := <-publish:
// Notify waiting list.
//for ch := waitingList.Back(); ch != nil; ch = ch.Prev() { // 獲取列表的最後一位
// ch.Value.(chan bool) <- true
// waitingList.Remove(ch)
//}
// 上面的方式存在問題,修改爲以下:
for ch := waitingList.Front(); ch != nil; ch = waitingList.Front() {
ch.Value.(chan bool) <- true
waitingList.Remove(ch)
}
broadcastWebSocket(event) // websocket 廣播此事件
models.NewArchive(event) // 將事件加入到檔案中
if event.Type == models.EVENT_MESSAGE {
logs.Info("Message from", event.User, ";Content:", event.Content)
}
case unsub := <-unsubscribe:
for sub := subscribers.Front(); sub != nil; sub = sub.Next() { // 遍歷訂閱者列表
if sub.Value.(Subscriber).Name == unsub { // 找到不訂閱的用戶
subscribers.Remove(sub) // 將此不訂閱用戶從訂閱者列表中移除
// Close connection.
ws := sub.Value.(Subscriber).Conn // 獲取此訂閱者的 ws 並關閉
if ws != nil {
ws.Close()
logs.Error("WebSocket closed:", unsub)
}
publish <- newEvent(models.EVENT_LEAVE, unsub, "") // Publish a LEAVE event. // 將一個 EVENT_LEAVE 事件放入到 publish 通道中
break
}
}
}
}
}
func init() {
go chatroom() // 啓動一個 goroutine
}
func isUserExist(subscribers *list.List, user string) bool { // 判斷用戶是否離開
for sub := subscribers.Front(); sub != nil; sub = sub.Next() {
if sub.Value.(Subscriber).Name == user {
return true
}
}
return false
}
複製代碼
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/logs"
"net/http"
"github.com/beego/samples/WebIM/models"
"github.com/gorilla/websocket"
)
// WebSocketController handles WebSocket requests.
type WebSocketController struct { // WebSocketController 封裝 baseController 的方法
baseController
}
// Get method handles GET requests for WebSocketController.
func (this *WebSocketController) Get() {
// Safe check.
uname := this.GetString("uname")
if len(uname) == 0 {
this.Redirect("/", 302)
return
}
this.TplName = "websocket.html" // 返回 websocket.html 頁面
this.Data["IsWebSocket"] = true // 設置返回頁面的變量值
this.Data["UserName"] = uname
}
// Join method handles WebSocket requests for WebSocketController.
func (this *WebSocketController) Join() {
uname := this.GetString("uname")
if len(uname) == 0 {
this.Redirect("/", 302)
return
}
// Upgrade from http request to WebSocket.
ws, err := websocket.Upgrade(this.Ctx.ResponseWriter, this.Ctx.Request, nil, 1024, 1024)
if _, ok := err.(websocket.HandshakeError); ok { // 檢查是否鏈接錯誤
http.Error(this.Ctx.ResponseWriter, "Not a websocket handshake", 400)
return
} else if err != nil {
logs.Error("Cannot setup WebSocket connection:", err)
return
}
// Join chat room.
Join(uname, ws) // 使用 chatroom 的 Join 方法,加入聊天室
defer Leave(uname)
// Message receive loop.
for { // 循環獲取 ws 接收的消息
_, p, err := ws.ReadMessage()
if err != nil {
return
}
publish <- newEvent(models.EVENT_MESSAGE, uname, string(p)) // 當接收到消息時,放入 publish 通道中
}
}
// broadcastWebSocket broadcasts messages to WebSocket users.
func broadcastWebSocket(event models.Event) { // 廣播信息
data, err := json.Marshal(event)
if err != nil {
logs.Error("Fail to marshal event:", err)
return
}
for sub := subscribers.Front(); sub != nil; sub = sub.Next() { // 遍歷全部訂閱者
// Immediately send event to WebSocket users.
ws := sub.Value.(Subscriber).Conn
if ws != nil {
if ws.WriteMessage(websocket.TextMessage, data) != nil { // 若是ws不存在則返回異常,證實已斷開鏈接
// User disconnected.
unsubscribe <- sub.Value.(Subscriber).Name // 當用戶斷開 websocket 鏈接,將此用戶放入到不訂閱通道中
}
}
}
}
複製代碼
Build web application with Golang