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