beego學習與代碼示例WebIM解析-Ali0th

Author : Ali0th

Date : 2019-4-26

安裝

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

下面我們從示例代碼 在線聊天室 WebIM 來學習 beego 吧。

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 # 啓動

在這裏插入圖片描述

問題與修復:

新版本 beego 沒有 beego.Info 、 beego.Error 、logs.Trace 等。

修復:

新版本 beego 把其引用方式修改了,放在了 logs 裏。只要把代碼修改成 logs.Info 、logs.Error 即可。

這裏使用 bash 腳本全局替換:

# 全部進行替換
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 連接。

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

長輪詢模式(Long Polling)與WebSocket模式

都是服務器推送技術(Push technology)中的一種。

長輪詢模式是一種 HTTP 短連接。長輪詢意味着瀏覽器只需啓動一個HTTP請求,其連接的服務器會“hold”住此次連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息後再向服務器發送新的Http請求,以此類推。

WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。

爲什麼 websocket 更好?
HTTP 是無狀態協議,HTTP通信只能由客戶端發起,HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。websocket 是有狀態協議,通信可以省略部分狀態信息。

Web通信中傳統輪詢、長輪詢和WebSocket簡介

WebSocket

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") // 加入

}

chatroom.go

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
}

websocket.go

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 連接,將此用戶放入到不訂閱通道中
			}
		}
	}
}

websocket js代碼

在這裏插入圖片描述

資料

Build web application with Golang

在 Chrome DevTools 中調試 JavaScript 入門

Golang Beego框架之WebIM例子分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章