一俯渤、WebSocket協(xié)議
在實現(xiàn)之前,我們需要解決一個底層問題型宝。
總所周知八匠,HTTP協(xié)議
是單向傳輸協(xié)議絮爷,只能由客戶端主動向服務端發(fā)送信息,反之則不行梨树。而在聊天室中坑夯,一個用戶發(fā)送一條消息,服務器則需要將該條消息廣播到聊天室中的所有用戶抡四,這想通過HTTP協(xié)議實現(xiàn)是不可能的柜蜈。
除非,讓每個用戶每隔一段時間便請求一次服務器獲取新消息指巡。這種方式稱為長輪詢淑履。但其缺點十分明顯,非常消耗資源藻雪。
為了解決這個問題秘噪,WebSocket協(xié)議
應運而生。
那什么是WebSocket協(xié)議
呢勉耀?百度百科
WebSocket協(xié)議
與HTTP協(xié)議
同屬于應用層協(xié)議缆娃。不同的是,WebSocket
是雙向傳輸協(xié)議瑰排,彌補了這個缺點贯要,在該協(xié)議下,服務端也能主動向客戶端發(fā)送信息椭住。同時崇渗,一旦連接,客戶端會與服務端保持長時間的通訊京郑。
WebSocket協(xié)議
的標識符是ws
宅广,如:ws://localhost:8080/chatRoom/WS
二、go語言并發(fā)特性
go
語言的一大特性些举,便是內(nèi)置的并發(fā)功能(goroutine
)跟狱。以及,在并發(fā)個體之間傳遞數(shù)據(jù)的“通道”(chan
)户魏。
具體細節(jié)不在此贅述驶臊。
三、beego框架
一個開源的輕量級web server
框架叼丑,實現(xiàn)了典型的MVC
模型关翎,和先進的api
接口模型(前后端分離模型)。
聊天室的實現(xiàn)鸠信,便基于其MVC
模型纵寝。
四、實現(xiàn)步驟
1.需求分析
1)數(shù)據(jù)分析
聊天室中主要物體分為兩種:用戶和消息星立。
用戶的主要屬性為:姓名爽茴、客戶端與服務端之間的WebSocket
連接指針葬凳。
消息則分為三種:用戶發(fā)消息、有用戶加入室奏、有用戶離開沮明。若將加入和離開也視為用戶發(fā)出的消息內(nèi)容,那消息的主要屬性就有:消息類型窍奋、消息內(nèi)容荐健、發(fā)消息者。
2)功能分析
前端:
- 實現(xiàn)與服務端的
WebSocket
連接琳袄。
后端:
提供
WebSocket
連接接口江场。與實現(xiàn)HTTP
連接接口一樣,利用beego
框架即可窖逗。當新用戶建立連接時址否、用戶斷開連接時、收到連接中用戶發(fā)來的新信息時碎紊,能將消息廣播給所有連接用戶佑附。
客戶端(即前端js)若要與服務端建立WebSocket
連接,需要調(diào)用WebSocket
連接API仗考,詳細內(nèi)容見大神博客音同。
服務端(即后端go)實現(xiàn)
2.數(shù)據(jù)結(jié)構(gòu)
用戶:
type Client struct {
conn *websocket.Conn // 用戶websocket連接
name string // 用戶名稱
}
消息:
// 1.設置為公開屬性(即首字母大寫),是因為屬性值私有時秃嗜,外包的函數(shù)無法使用或訪問該屬性值(如:json.Marshal())
// 2.`json:"name"` 是為了在對該結(jié)構(gòu)類型進行json編碼時权均,自定義該屬性的名稱
type Message struct {
EventType byte `json:"type"` // 0表示用戶發(fā)布消息;1表示用戶進入锅锨;2表示用戶退出
Name string `json:"name"` // 用戶名稱
Message string `json:"message"` // 消息內(nèi)容
}
用戶組:
clients = make(map [Client] bool) // 用戶組映射
此處使用映射而不是數(shù)組叽赊,是為了方便判斷某個用戶是否已經(jīng)加入或者已經(jīng)退出了。
用于goroutine
通道:
// 此處要設置有緩沖的通道必搞。因為這是goroutine自己從通道中發(fā)送并接受數(shù)據(jù)必指。
// 若是無緩沖的通道,該goroutine發(fā)送數(shù)據(jù)到通道后就被鎖定恕洲,需要數(shù)據(jù)被接受后才能解鎖塔橡,而恰恰接受數(shù)據(jù)的又只能是它自己
join = make(chan Client, 10) // 用戶加入通道
leave = make(chan Client, 10) // 用戶退出通道
message = make(chan Message, 10) // 消息通道
3.功能實現(xiàn)
1)前端WebSocket
連接實現(xiàn):
//====================webSocket連接======================
// 創(chuàng)建一個webSocket連接
var socket = new WebSocket('ws://'+window.location.host+'/chatRoom/WS?name=' + $('#name').text());
// 當webSocket連接成功的回調(diào)函數(shù)
socket.onopen = function () {
console.log("webSocket open");
connected = true;
};
// 斷開webSocket連接的回調(diào)函數(shù)
socket.onclose = function () {
console.log("webSocket close");
connected = false;
};
//=======================接收消息并顯示===========================
// 接受webSocket連接中,來自服務端的消息
socket.onmessage = function(event) {
// 將服務端發(fā)送來的消息進行json解析
var data = JSON.parse(event.data);
console.log("revice:" , data);
var name = data.name;
var type = data.type;
var msg = data.message;
// type為0表示有人發(fā)消息
var $messageDiv;
if (type == 0) {
var $usernameDiv = $('<span style="margin-right: 15px;font-weight: 700;overflow: hidden;text-align: right;"/>')
.text(name);
if (name == $("#name").text()) {
$usernameDiv.css('color', nameColor);
} else {
$usernameDiv.css('color', getUsernameColor(name));
}
var $messageBodyDiv = $('<span style="color: gray;"/>')
.text(msg);
$messageDiv = $('<li style="list-style-type:none;font-size:25px;"/>')
.data('username', name)
.append($usernameDiv, $messageBodyDiv);
}
// type為1或2表示有人加入或退出
else {
var $messageBodyDiv = $('<span style="color:#999999;">')
.text(msg);
$messageDiv = $('<li style="list-style-type:none;font-size:15px;text-align:center;"/>')
.append($messageBodyDiv);
}
$messageArea.append($messageDiv);
$messageArea[0].scrollTop = $messageArea[0].scrollHeight; // 讓屏幕滾動
}
//========================發(fā)送消息==========================
// 通過webSocket發(fā)送消息到服務端
function sendMessage () {
var inputMessage = $inputArea.val(); // 獲取輸入框的值
if (inputMessage && connected) {
$inputArea.val(''); // 清空輸入框的值
socket.send(inputMessage); // 基于WebSocket連接發(fā)送消息
console.log("send message:" + inputMessage);
}
}
2)后端WebSocket
連接接口
繼承beego
框架的Controller
類型:
type ServerController struct {
beego.Controller
}
編寫ServerController
類型中用于WebSocket
連接的方法:
// 用于與用戶間的websocket連接(chatRoom.html發(fā)送來的websocket請求)
func (c *ServerController) WS() {
name := c.GetString("name")
if len(name) == 0 {
beego.Error("name is NULL")
c.Redirect("/", 302)
return
}
// 檢驗http頭中upgrader屬性研侣,若為websocket谱邪,則將http協(xié)議升級為websocket協(xié)議
conn, err := (&websocket.Upgrader{}).Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil)
if _, ok := err.(websocket.HandshakeError); ok {
beego.Error("Not a websocket connection")
http.Error(c.Ctx.ResponseWriter, "Not a websocket handshake", 400)
return
} else if err != nil {
beego.Error("Cannot setup WebSocket connection:", err)
return
}
var client Client
client.name = name
client.conn = conn
// 如果用戶列表中沒有該用戶
if !clients[client] {
join <- client
beego.Info("user:", client.name, "websocket connect success!")
}
// 當函數(shù)返回時,將該用戶加入退出通道庶诡,并斷開用戶連接
defer func() {
leave <- client
client.conn.Close()
}()
// 由于WebSocket一旦連接,便可以保持長時間通訊咆课,則該接口函數(shù)可以一直運行下去末誓,直到連接斷開
for {
// 讀取消息扯俱。如果連接斷開,則會返回錯誤
_, msgStr, err := client.conn.ReadMessage()
// 如果返回錯誤喇澡,就退出循環(huán)
if err != nil {
break
}
beego.Info("WS-----------receive: "+string(msgStr))
// 如果沒有錯誤迅栅,則把用戶發(fā)送的信息放入message通道中
var msg Message
msg.Name = client.name
msg.EventType = 0
msg.Message = string(msgStr)
message <- msg
}
}
3)后端廣播功能
將發(fā)消息、用戶加入晴玖、用戶退出三種情況都廣播給所有用戶读存。后兩種情況經(jīng)過處理,轉(zhuǎn)換為第一種情況呕屎。真正發(fā)送信息給客戶端的让簿,只有第一種情況。
func broadcaster() {
for {
// 哪個case可以執(zhí)行秀睛,則轉(zhuǎn)入到該case尔当。若都不可執(zhí)行,則堵塞蹂安。
select {
// 消息通道中有消息則執(zhí)行椭迎,否則堵塞
case msg := <-message:
str := fmt.Sprintf("broadcaster-----------%s send message: %s\n", msg.Name, msg.Message)
beego.Info(str)
// 將某個用戶發(fā)出的消息發(fā)送給所有用戶
for client := range clients {
// 將數(shù)據(jù)編碼成json形式,data是[]byte類型
// json.Marshal()只會編碼結(jié)構(gòu)體中公開的屬性(即大寫字母開頭的屬性)
data, err := json.Marshal(msg)
if err != nil {
beego.Error("Fail to marshal message:", err)
return
}
// fmt.Println("=======the json message is", string(data)) // 轉(zhuǎn)換成字符串類型便于查看
if client.conn.WriteMessage(websocket.TextMessage, data) != nil {
beego.Error("Fail to write message")
}
}
// 有用戶加入
case client := <-join:
str := fmt.Sprintf("broadcaster-----------%s join in the chat room\n", client.name)
beego.Info(str)
clients[client] = true // 將用戶加入映射
// 將用戶加入消息放入消息通道
var msg Message
msg.Name = client.name
msg.EventType = 1
msg.Message = fmt.Sprintf("%s join in, there are %d preson in room", client.name, len(clients))
// 此處要設置有緩沖的通道田盈。因為這是goroutine自己從通道中發(fā)送并接受數(shù)據(jù)畜号。
// 若是無緩沖的通道,該goroutine發(fā)送數(shù)據(jù)到通道后就被鎖定允瞧,需要數(shù)據(jù)被接受后才能解鎖弄兜,而恰恰接受數(shù)據(jù)的又只能是它自己
message <- msg
// 有用戶退出
case client := <-leave:
str := fmt.Sprintf("broadcaster-----------%s leave the chat room\n", client.name)
beego.Info(str)
// 如果該用戶已經(jīng)被刪除
if !clients[client] {
beego.Info("the client had leaved, client's name:"+client.name)
break
}
delete(clients, client) // 將用戶從映射中刪除
// 將用戶退出消息放入消息通道
var msg Message
msg.Name = client.name
msg.EventType = 2
msg.Message = fmt.Sprintf("%s leave, there are %d preson in room", client.name, len(clients))
message <- msg
}
}
}
在后端服務啟動時,便開啟廣播功能:
func init() {
go broadcaster()
}
此處需要利用goroutine
并發(fā)模式瓷式,使得該函數(shù)能獨立在額外的一個線程上運作替饿。
五、參考文檔
感謝作者:99MyCql
查看原文:用go實現(xiàn)聊天室(WebSocket方式)
添加客服微信:grey0805贸典,加入閱讀小分隊