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例子分析

你可能感兴趣的:(beego学习与代码示例WebIM解析-Ali0th)