用go語言實現(xiàn)聊天室 (WebSocket協(xié)議)

一俯渤、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贸典,加入閱讀小分隊

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末视卢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子廊驼,更是在濱河造成了極大的恐慌据过,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妒挎,死亡現(xiàn)場離奇詭異绳锅,居然都是意外死亡,警方通過查閱死者的電腦和手機酝掩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門鳞芙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事原朝⊙敝觯” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵喳坠,是天一觀的道長鞠评。 經(jīng)常有香客問我,道長壕鹉,這世上最難降的妖魔是什么剃幌? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮晾浴,結(jié)果婚禮上负乡,老公的妹妹穿的比我還像新娘。我一直安慰自己怠肋,他們只是感情好敬鬓,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笙各,像睡著了一般钉答。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杈抢,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天数尿,我揣著相機與錄音,去河邊找鬼惶楼。 笑死右蹦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的歼捐。 我是一名探鬼主播何陆,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼豹储!你這毒婦竟也來了贷盲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤剥扣,失蹤者是張志新(化名)和其女友劉穎巩剖,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钠怯,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡佳魔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晦炊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鞠鲜。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡宁脊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镊尺,到底是詐尸還是另有隱情朦佩,我是刑警寧澤并思,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布庐氮,位于F島的核電站,受9級特大地震影響宋彼,放射性物質(zhì)發(fā)生泄漏绍傲。R本人自食惡果不足惜挡毅,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚜迅,春花似錦、人聲如沸玫氢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檐什。三九已至碴卧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乃正,已是汗流浹背住册。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓮具,地道東北人荧飞。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像名党,于是被迫代替她去往敵國和親叹阔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344