教你如何用Golang打造實時聊天系統

項目截圖


簡介

在本次課程中,我們來學習使用WebSocket來打造一個實時聊天系統。我們會從一下幾個方面來進行學習:

什么是websocket;

Websocket與傳統的HTTP協議有什么區別;

Websocket有哪些優點;

如何建立連接;

如何維持連接;

Golang實戰項目—實時聊天系統;

總結;

什么是websocket?

WebSocket協議是基于TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發送信息給客戶端。

WebSocket通信協議于2011年被IETF定為標準RFC 6455,并被RFC7936所補充規范。

WebSocket協議支持(在受控環境中運行不受信任的代碼的)客戶端與(選擇加入該代碼的通信的)遠程主機之間進行全雙工通信。用于此的安全模型是Web瀏覽器常用的基于原始的安全模式。 協議包括一個開放的握手以及隨后的TCP層上的消息幀。 該技術的目標是為基于瀏覽器的、需要和服務器進行雙向通信的(服務器不能依賴于打開多個HTTP連接(例如,使用XMLHttpRequest或iframe和長輪詢))應用程序提供一種通信機制。

Websocket與傳統的HTTP協議有什么區別?

http,websocket都是應用層協議,他們規定的是數據怎么封裝,而他們傳輸的通道是下層提供的。就是說無論是 http 請求,還是 WebSocket 請求,他們用的連接都是傳輸層提供的,即 tcp 連接(傳輸層還有 udp 連接)。只是說 http1.0 協議規定,你一個請求獲得一個響應后,你要把連接關掉。所以你用 http 協議發送的請求是無法做到一直連著的(如果服務器一直不返回也可以保持相當一段時間,但是也會有超時而被斷掉)。而 WebSocket 協議規定說等握手完成后我們的連接不能斷哈。雖然 WebSocket 握手用的是 http 請求,但是請求頭和響應頭里面都有特殊字段,當瀏覽器或者服務端收到后會做相應的協議轉換。所以 http 請求被 hold 住不返回的長連接和 WebSocket 的連接是有本質區別的。

WebSocket有哪些優點?

說到優點,這里的對比參照物是HTTP協議,概括地說就是:支持雙向通信,更靈活,更高效,可擴展性更好。

支持雙向通信,實時性更強。

更好的二進制支持。

較少的控制開銷。連接創建后,ws客戶端、服務端進行數據交換時,協議控制的數據包頭部較小。在不包含頭部的情況下,服務端到客戶端的包頭只有2~10字節(取決于數據包長度),客戶端到服務端的的話,需要加上額外的4字節的掩碼。而HTTP協議每次通信都需要攜帶完整的頭部。

支持擴展。ws協議定義了擴展,用戶可以擴展協議,或者實現自定義的子協議。(比如支持自定義壓縮算法等)

對于后面兩點,沒有研究過WebSocket協議規范的同學可能理解起來不夠直觀,但不影響對WebSocket

如何建立連接?

客戶端通過HTTP請求與WebSocket服務端協商升級協議。協議升級完成后,后續的數據交換則遵照WebSocket的協議。

1. 客戶端:申請協議升級

首先,客戶端發起協議升級請求。可以看到,采用的是標準的HTTP報文格式,且只支持GET方法。

GET / HTTP/1.1

Host: localhost:8080

Origin: http://127.0.0.1:3000

Connection: Upgrade

Upgrade: websocket

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

重點請求首部意義如下:

Connection: Upgrade:表示要升級協議

Upgrade: websocket:表示要升級到websocket協議。

Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,里面包含服務端支持的版本號。

Sec-WebSocket-Key:與后面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。

2. 服務器:響應協議升級

服務端返回內容如下,狀態代碼101表示協議切換。到此完成協議升級,后續的數據交互都按照新的協議來。

HTTP/1.1 101 Switching Protocols

Connection:Upgrade

Upgrade: websocket

Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=


3. Sec-WebSocket-Accept的計算

Sec-WebSocket-Accept根據客戶端請求首部的Sec-WebSocket-Key計算出來。

計算公式為:

將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。

通過SHA1計算出摘要,并轉成base64字符串。

如何維持連接?

WebSocket為了保持客戶端、服務端的實時雙向通信,需要確保客戶端、服務端之間的TCP通道保持連接沒有斷開。然而,對于長時間沒有數據往來的連接,如果依舊長時間保持著,可能會浪費包括的連接資源。

但不排除有些場景,客戶端、服務端雖然長時間沒有數據往來,但仍需要保持連接。這個時候,可以采用心跳來實現。

發送方->接收方:ping

接收方->發送方:pong

ping、pong的操作,對應的是WebSocket的兩個控制幀,opcode分別是0x9、0xA。

舉例,WebSocket服務端向客戶端發送ping,只需要如下代碼(采用ws模塊)

Golang實戰項目—實時聊天系統

這里是使用Github上的一個開源項目作為案例。

獲取Golang的websocket庫

go get github.com/gorilla/websocket


獲取測試程序

git clone https://github.com/scotch-io/go-realtime-chat.git


Server

package main

import (

? ? "log"

? ? "net/http"

? ? "github.com/gorilla/websocket"

)

var clients = make(map[*websocket.Conn]bool) // connected clients

var broadcast = make(chan Message)? ? ? ? ? // broadcast channel

// Configure the upgrader

var upgrader = websocket.Upgrader{

? ? CheckOrigin: func(r *http.Request) bool {

? ? ? ? return true

? ? },

}

// Define our message object

type Message struct {

? ? Email? ? string `json:"email"`

? ? Username string `json:"username"`

? ? Message? string `json:"message"`

}

func main() {

? ? // Create a simple file server

? ? fs := http.FileServer(http.Dir("../public"))

? ? http.Handle("/", fs)

? ? // Configure websocket route

? ? http.HandleFunc("/ws", handleConnections)

? ? // Start listening for incoming chat messages

? ? go handleMessages()

? ? // Start the server on localhost port 8000 and log any errors

? ? log.Println("http server started on :8000")

? ? err := http.ListenAndServe(":8000", nil)

? ? if err != nil {

? ? ? ? log.Fatal("ListenAndServe: ", err)

? ? }

}

func handleConnections(w http.ResponseWriter, r *http.Request) {

? ? // Upgrade initial GET request to a websocket

? ? ws, err := upgrader.Upgrade(w, r, nil)

? ? if err != nil {

? ? ? ? log.Fatal(err)

? ? }

? ? // Make sure we close the connection when the function returns

? ? defer ws.Close()

? ? // Register our new client

? ? clients[ws] = true

? ? for {

? ? ? ? var msg Message

? ? ? ? // Read in a new message as JSON and map it to a Message object

? ? ? ? err := ws.ReadJSON(&msg)

? ? ? ? if err != nil {

? ? ? ? ? ? log.Printf("error: %v", err)

? ? ? ? ? ? delete(clients, ws)

? ? ? ? ? ? break

? ? ? ? }

? ? ? ? // Send the newly received message to the broadcast channel

? ? ? ? broadcast <- msg

? ? }

}

func handleMessages() {

? ? for {

? ? ? ? // Grab the next message from the broadcast channel

? ? ? ? msg := <-broadcast

? ? ? ? // Send it out to every client that is currently connected

? ? ? ? for client := range clients {

? ? ? ? ? ? err := client.WriteJSON(msg)

? ? ? ? ? ? if err != nil {

? ? ? ? ? ? ? ? log.Printf("error: %v", err)

? ? ? ? ? ? ? ? client.Close()

? ? ? ? ? ? ? ? delete(clients, client)

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

客戶端

new Vue({

? ? el: '#app',

? ? data: {

? ? ? ? ws: null, // Our websocket

? ? ? ? newMsg: '', // Holds new messages to be sent to the server

? ? ? ? chatContent: '', // A running list of chat messages displayed on the screen

? ? ? ? email: null, // Email address used for grabbing an avatar

? ? ? ? username: null, // Our username

? ? ? ? joined: false // True if email and username have been filled in

? ? },

? ? created: function() {

? ? ? ? var self = this;

? ? ? ? this.ws = new WebSocket('ws://' + window.location.host + '/ws');

? ? ? ? this.ws.addEventListener('message', function(e) {

? ? ? ? ? ? var msg = JSON.parse(e.data);

? ? ? ? ? ? self.chatContent += '<div class="chip">'

? ? ? ? ? ? ? ? ? ? + '<img src="' + self.gravatarURL(msg.email) + '">' // Avatar

? ? ? ? ? ? ? ? ? ? + msg.username

? ? ? ? ? ? ? ? + '</div>'

? ? ? ? ? ? ? ? + emojione.toImage(msg.message) + '<br/>'; // Parse emojis

? ? ? ? ? ? var element = document.getElementById('chat-messages');

? ? ? ? ? ? element.scrollTop = element.scrollHeight; // Auto scroll to the bottom

? ? ? ? });

? ? },

? ? methods: {

? ? ? ? send: function () {

? ? ? ? ? ? if (this.newMsg != '') {

? ? ? ? ? ? ? ? this.ws.send(

? ? ? ? ? ? ? ? ? ? JSON.stringify({

? ? ? ? ? ? ? ? ? ? ? ? email: this.email,

? ? ? ? ? ? ? ? ? ? ? ? username: this.username,

? ? ? ? ? ? ? ? ? ? ? ? message: $('<p>').html(this.newMsg).text() // Strip out html

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ));

? ? ? ? ? ? ? ? this.newMsg = ''; // Reset newMsg

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? join: function () {

? ? ? ? ? ? if (!this.email) {

? ? ? ? ? ? ? ? Materialize.toast('You must enter an email', 2000);

? ? ? ? ? ? ? ? return

? ? ? ? ? ? }

? ? ? ? ? ? if (!this.username) {

? ? ? ? ? ? ? ? Materialize.toast('You must choose a username', 2000);

? ? ? ? ? ? ? ? return

? ? ? ? ? ? }

? ? ? ? ? ? this.email = $('<p>').html(this.email).text();

? ? ? ? ? ? this.username = $('<p>').html(this.username).text();

? ? ? ? ? ? this.joined = true;

? ? ? ? },

? ? ? ? gravatarURL: function(email) {

? ? ? ? ? ? return 'http://www.gravatar.com/avatar/' + CryptoJS.MD5(email);

? ? ? ? }

? ? }

});

總結

具體使用什么技術是需要根據使用場景進行選擇的,在這里我為大家總結了http和websocket不同的使用場景,請大家參考。

HTTP :

檢索資源(Retrieve Resource)

高度可緩存的資源(Highly Cacheable Resource)

冪等性和安全性(Idempotency and Safety)

錯誤方案(Error Scenarios)

websockt

快速響應時間(Fast Reaction Time)

持續更新(Ongoing Updates)

Ad-hoc消息傳遞(Ad-hoc Messaging)

錯誤的HTTP應用場景

依賴于客戶端輪詢服務,而不是由用戶主動發起。

需要頻繁的服務調用來發送小消息。

客戶端需要快速響應對資源的更改,并且,無法預測更改何時發生。

錯誤的WebSockets應用場景

連接僅用于極少數事件或非常短的時間,客戶端無需快速響應事件。

需要一次打開多個WebSockets到同一服務。

打開WebSocket,發送消息,然后關閉它 - 然后再重復該過程。

消息傳遞層中重新實現請求/響應模式。

以上內容都是我自己的一些感想,分享出來歡迎大家指正,順便求一波關注,有問題的或者更好的想法的小伙伴可以評論私信我哦~或者點擊Java學習交流群一起交流聊天!

作者:DailyProgrammer

鏈接:https://zhuanlan.zhihu.com/p/81694216

來源:知乎

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容