現(xiàn)代網(wǎng)頁應(yīng)用程序正日趨豐富而復(fù)雜。像這樣有趣又有活力的體驗很受用戶歡迎幻锁。用戶無需向服務(wù)器發(fā)起調(diào)用富拗,或刷新瀏覽器,就可以讓頁面實時更新窄锅。早期的開發(fā)者依賴 AJAX 來創(chuàng)建具備近乎實時體驗的應(yīng)用程序创千。而現(xiàn)在,他們運用 WebSockets 就能創(chuàng)建完全實時的應(yīng)用程序了。
本教程中我們將使用 Go 編程語言以及 WebSockets 來創(chuàng)建一個實時的聊天應(yīng)用程序追驴。前端將會使用 HTML5 和 VueJS 來編寫械哟。該內(nèi)容需要你對 Go 語言, JavaScript 以及 HTML5 有一個基礎(chǔ)的了解,最好有一點點使用 VueJS 的經(jīng)驗殿雪。
WebSocket 是什么?
通常 Web 應(yīng)用使用一個或多個請求對 HTTP 服務(wù)器提供對外服務(wù)暇咆。客戶端軟件通常是 Web 瀏覽器向服務(wù)器發(fā)送請求冠摄,服務(wù)器發(fā)回一個響應(yīng)糯崎。響應(yīng)通常是 HTML 內(nèi)容,由瀏覽器來渲染為頁面河泳。樣式表沃呢,JavaScript 代碼和圖像也可以在響應(yīng)中發(fā)送回來以完成整個網(wǎng)頁。每個請求和響應(yīng)都屬于特定的單獨的連接的一部分拆挥,像 Facebook 這樣的大型網(wǎng)站為了渲染單個頁面實際上可以產(chǎn)生數(shù)百個這樣的連接薄霜。
AJAX 的工作方式跟這個完全相同。使用 JavaScript纸兔,開發(fā)人員可以向 HTTP 服務(wù)器請求一小段信息惰瓜,然后根據(jù)響應(yīng)更新部分頁面。這可以在不刷新瀏覽器的情況下完成汉矿,但仍然存在一些限制崎坊。
每個 HTTP 請求/響應(yīng)的連接在被響應(yīng)之后都會關(guān)閉,因此獲得任何新的信息必須新建另一個連接洲拇。如果沒有新的請求發(fā)送給服務(wù)器奈揍,它就不知道客戶端正在查找新的信息。能讓 AJAX 應(yīng)用程序看起來像實時的一種技術(shù)是定時循環(huán)發(fā)送 AJAX 請求赋续。在設(shè)置了時間間隔之后男翰,應(yīng)用程序可以重新將請求發(fā)送到服務(wù)器,以查看是否有任何更新需要反饋給瀏覽器纽乱。這比較適合小型應(yīng)用程序蛾绎,但并不高效。這時候 WebSockets 就派上用場了鸦列。WebSockets 是由 Internet 工程任務(wù)組(IETF)創(chuàng)建的建議標(biāo)準(zhǔn)的一部分租冠。 RFC6455 中詳細(xì)描述了 WebSockets 實現(xiàn)的完整技術(shù)規(guī)范。下面是該文檔定義 WebSocket 的節(jié)選:
”
WebSocket 協(xié)議用于客戶端代碼和遠(yuǎn)程主機(jī)之間進(jìn)行通信薯嗤,其中客戶端代碼是在可控環(huán)境下的非授信代碼
換句話說肺稀,WebSocket 是一個總是打開的連接,允許客戶端和服務(wù)器自發(fā)地來回發(fā)送消息应民。服務(wù)器可在必要時將新信息推送到客戶端,客戶端也可以對服務(wù)器執(zhí)行相同操作。
JavaScript 中的 WebSockets
大多數(shù)現(xiàn)代瀏覽器都在其 JavaScript 實現(xiàn)中支持 WebSockets诲锹。要從瀏覽器中啟動一個 WebSocket 連接繁仁,你可以使用簡單的 WebSocket JavaScript 對象,如下:
您唯一需要的參數(shù)是一個 URL归园,WebSocket 連接可通過此 URL 連接服務(wù)器黄虱。該請求實際是一個 HTTP 請求,但為了安全連接我們使用“ws://”或“wss://”庸诱。這使服務(wù)器知道我們正在嘗試創(chuàng)建一個新的 WebSocket 連接捻浦。之后服務(wù)器將“升級”該客戶端和服務(wù)之間的連接到永久的雙向連接。一旦新的 WebSocket 對象被創(chuàng)建桥爽,并且連接成功創(chuàng)建之后朱灿,我們就可以使用“send()”方法發(fā)送文本到服務(wù)器,并在 WebSocket 的“onmessage”屬性上定義一個處理函數(shù)來處理從服務(wù)器發(fā)送的消息钠四。具體邏輯會在之后的聊天應(yīng)用程序代碼中解釋盗扒。
Go 中的 WebSockets
WebSockets 并不包含在 Go 標(biāo)準(zhǔn)庫中,但幸運的是有一些不錯的第三方包讓 WebSockets 的使用輕而易舉缀去。在這個例子中侣灶,我們將使用一個名為“gorilla/websocket”的包,它是流行的 Gorilla Toolkit 包集合的一部分缕碎,多用于在 Go 中創(chuàng)建 Web 應(yīng)用程序褥影。請運行以下命令進(jìn)行安裝:
構(gòu)建服務(wù)器
這個應(yīng)用程序的第一部分是服務(wù)器。這是一個處理請求的簡單 HTTP 服務(wù)器咏雌。它將為我們提供 HTML5 和 JavaScript 代碼凡怎,以及建立客戶端的 WebSocket 連接。另外处嫌,服務(wù)器還將跟蹤每個 WebSocket 連接并通過 WebSocket 連接將聊天信息從一個客戶端發(fā)送到所有其他客戶端栅贴。首先創(chuàng)建一個新的空目錄,然后在該目錄中創(chuàng)建一個“src”和“public”目錄熏迹。在“src”目錄中創(chuàng)建一個名為“main.go”的文件檐薯。搭建服務(wù)器首先要進(jìn)行一些設(shè)置。我們像所有 Go 應(yīng)用程序一樣啟動應(yīng)用程序注暗,并定義包命名空間坛缕,在本例中為“main”。接下來我們導(dǎo)入一些有用的包捆昏。 “l(fā)og”和“net/http”都是標(biāo)準(zhǔn)庫的一部分赚楚,將用于日志記錄并創(chuàng)建一個簡單的 HTTP 服務(wù)器。最終包“http://github.com/gorilla/websocket”將幫助我們輕松創(chuàng)建和使用 WebSocket 連接骗卜。
下面的兩行代碼是一些全局變量宠页,在應(yīng)用程序的其它地方會被用到左胞。全局變量的實踐較差,不過這次為了簡單起見我們還是使用了它們举户。第一個變量是一個 map 映射烤宙,其鍵對應(yīng)是一個指向 WebSocket 的指針,其值就是一個布爾值俭嘁。我們實際上并不需要這個值躺枕,但使用的映射數(shù)據(jù)結(jié)構(gòu)需要有一個映射值,這樣做更容易添加和刪除單項供填。
第二個變量是一個用于由客戶端發(fā)送消息的隊列拐云,扮演通道的角色。在后面的代碼中叉瘩,我們會定義一個 goroutine 來從這個通道讀取新消息,然后將它們發(fā)送給其它連接到服務(wù)器的客戶端晌端。
接下來我們創(chuàng)建一個 upgrader 的實例。這只是一個對象梧奢,它具備一些方法,這些方法可以獲取一個普通 HTTP 鏈接然后將其升級成一個 WebSocket,稍后會有相關(guān)代碼介紹。
最后我們將定義一個對象來管理消息,數(shù)據(jù)結(jié)構(gòu)比較簡單库正,帶有一些字符串屬性,一個 email 地址载迄,一個用戶名以及實際的消息內(nèi)容。我們將利用 email 來展示 Gravatar 服務(wù)所提供的唯一身份標(biāo)識捣炬。
由反引號包含的文本是 Go 在對象和 JSON 之間進(jìn)行序列化和反序列化時需要的元數(shù)據(jù)推溃。
Go 應(yīng)用程序的主要入口總是 "main()" 函數(shù)硬萍。代碼非常簡潔短曾。我們首先創(chuàng)建一個靜態(tài)的文件服務(wù),并將之與 "/" 路由綁定咐汞,這樣用戶訪問網(wǎng)站時就能看到 index.html 和其它資源植阴。在這個示例中我們有一個保存 JavaScript 代碼的 "app.js" 文件和一個保存樣式的 "style.css" 文件喷鸽。
我們想定義的下一個路由是 "/ws",在這里處理啟動 WebSocket 的請求纤房。我們先向處理函數(shù)傳遞一個函數(shù)的名稱纵隔,"handleConnections"捌刮,稍后再來定義這個函數(shù)俄认。
下一步就是啟動一個叫 "handleMessages" 的 Go 程序萎津。這是一個并行過程,獨立于應(yīng)用和其它部分運行锉屈,從廣播頻道中取得消息并通過各客戶端的 WebSocket 連接傳遞出去遂黍。并行是 Go 中一項強(qiáng)大的特性。關(guān)于它如何工作的內(nèi)容超出了這篇文章的范圍俊嗽,不過你可以自行查看 Go 的官方教程網(wǎng)站妓湘。如果你熟悉 JavaScript,可聯(lián)想一下并行過程乌询,作為后臺過程運行的 Go 程序,或 JavaScript 的異步函數(shù)豌研。
最后妹田,我們向控制臺打印一個輔助信息并啟動 Web 服務(wù)。如果有錯誤發(fā)生鹃共,我們就把它記錄下來然后退出應(yīng)用程序鬼佣。
接下來我們創(chuàng)建一個函數(shù)處理傳入的 WebSocket 連接。首先我們使用升級的 "Upgrade()" 方法改變初始的 GET 請求霜浴,使之成為完全的 WebSocket晶衷。如果發(fā)生錯誤,記錄下來阴孟,但不退出晌纫。同時注意 defer 語句,它通知 Go 在函數(shù)返回的時候關(guān)閉 WebSocket永丝。這是個不錯的方法锹漱,它為我們節(jié)省了不少可能出現(xiàn)在不同分支中返回函數(shù)前的 "Close()" 語句。
接下來把新的客戶端添加到全局的 "clients" 映射表中進(jìn)行注冊慕嚷,這個映射表在早先已經(jīng)創(chuàng)建了哥牍。
最后一步是一個無限循環(huán),它一直等待著要寫入 WebSocket 的新消息喝检,將其從 JSON 反序列化為 Message 對象然后送入廣播頻道嗅辣。然而 "handleMessages()" Go 程序就能把它送給連接中的其它客戶端。
如果從 socket 中讀取數(shù)據(jù)有誤挠说,我們假設(shè)客戶端已經(jīng)因為某種原因斷開澡谭。我們記錄錯誤并從全局的 “clients” 映射表里刪除該客戶端,這樣一來纺涤,我們不會繼續(xù)嘗試與其通信译暂。
另外抠忘,HTTP 路由處理函數(shù)已經(jīng)被作為 goroutines 運行。這使得 HTTP 服務(wù)器無需等待另一個連接完成外永,就能處理多個傳入連接崎脉。
服務(wù)器的最后一部分是 "handleMessages()"函數(shù)。這是一個簡單循環(huán)伯顶,從“broadcast”中連續(xù)讀取數(shù)據(jù)囚灼,然后通過各自的 WebSocket 連接將消息傳播到所以客戶端。同樣祭衩,如果寫入 Websocket 時出現(xiàn)錯誤灶体,我們將關(guān)閉連接,并將其從“clients” 映射中刪除掐暮。
構(gòu)建客戶端
如果沒有漂亮的 UI蝎抽,聊天應(yīng)用程序?qū)o法完成。 我們需要使用一些 HTML5 和 VueJS 來創(chuàng)建一個簡單路克、干凈的界面樟结,再利用一些諸如 Materialize CSS 和 EmojiOn 的庫來生成一些樣式和表情符號。 在“public”目錄中精算,創(chuàng)建一個名為“index.html”的新文件瓢宦。第一部分很基礎(chǔ)。為了美觀灰羽,我們也會放入一些樣式表和字體驮履。“style.css”是自定義的樣式表廉嚼,用于自定義一些內(nèi)容玫镐。
下一部分僅與接口相關(guān),其中只包含一些用于選擇用戶名前鹅、發(fā)送消息和顯示新的聊天信息的字段摘悴。與 VueJS 交互的細(xì)節(jié)超出本文的介紹范圍,你可閱讀此文檔了解更多舰绘。
最后一步只需要導(dǎo)入所有需要的 JavaScript 庫蹂喻,包括 Vue、EmojiOne捂寿、jQuery 和 Materialize口四。我們需要 MD5 庫獲取來自 Gravatar 的頭像 URL,這用 JavaScript 代碼寫出來就好理解了秦陋。最后導(dǎo)入 "app.js"蔓彩。
然后在 "public" 目錄下創(chuàng)建一個 "style.css" 文件。其中會放入一些樣式。
客戶端的最后一部分是 JavaScript 代碼赤嚼。在 "public" 目錄下創(chuàng)建文件 "app.js"旷赖。
對于 VueJS 應(yīng)用程序來說,一開始都是創(chuàng)建新的 Vue 對象更卒。我們將它與 id 為 "#app" 的 div 綁定等孵。這會讓 div 內(nèi)的所有東西與 Vue 實現(xiàn)共享作用域。下面定義一些變量蹂空。
Vue 提供了一個叫 "created" 的屬性俯萌,這是一個函數(shù),會在 Vue 實例剛剛創(chuàng)建時調(diào)用上枕。這里非常適合對應(yīng)用做一些設(shè)置工作咐熙。在這個示例中我們希望創(chuàng)建一個新的 WebSocket 連接與服務(wù)器連接,并創(chuàng)建一個處理器用于處理從服務(wù)器發(fā)送過來的消息辨萍。我們把新的 WebSocket 對象保存在 "data" 屬性的 "ws" 變量中棋恼。
"addEventListener()"方法接受一個用于處理傳入消息的函數(shù)。我們期望所有消息都是 JSON 字符串锈玉,以便統(tǒng)一解析為一個對象字面量蘸泻。然后我們可以用各個屬性和 avater 頭像一起組成漂亮的消息行。"gravatarURL()" 方法會在后面詳述嘲玫。我們用了一個叫 EmojiOne 的表情庫來解析emoji 代碼。"toImage()" 方法會把 emoji 代碼轉(zhuǎn)換為實際的圖片并扇。比如去团,如果你輸入 ":robot:",它會被替換為一個機(jī)器人 emoji 表情圖穷蛹。
"methods" 屬性可以定義各種函數(shù)土陪,我們會在 VueJS 應(yīng)用中使用這些函數(shù)。"send"方法用于向服務(wù)器發(fā)送消息肴熏。我們先確保消息不是空的鬼雀,然后把消息組織成一個對象,再用"stringify"把它變成 JSON 字符串蛙吏,以便服務(wù)器能正確解析源哩。我們使用 jQuery 來處理傳入消息中 HTML 和 JavaScript 中的特殊字符,以防止各種類型的注入攻擊鸦做。
"join"函數(shù)會確保用戶在發(fā)送消息前輸入 email 地址和用戶名励烦。一旦他們輸入了這些信息,我們將 joined 設(shè)置為 "true"泼诱,同時允許他們開始交談坛掠。同樣,我們會處理 HTML 和 JavaScript 的特殊字符。
最后一個函數(shù)是一個很好的輔助函數(shù)屉栓,用于從 Gravatar 獲取頭像舷蒲。URL 的最后一段需要用戶的 email 地址的 MD5 編碼。MD5 是一種加密算法友多,它能隱藏 email 地址同時還能讓 email 地址作為一個唯一標(biāo)識來使用牲平。
運行應(yīng)用程序
要運行該應(yīng)用程序,請打開控制臺窗口并確保進(jìn)入應(yīng)用程序的“src”目錄中夷陋,然后運行以下命令欠拾。
接下來打開 Web 瀏覽器并導(dǎo)航到“http://localhost:8000”站點。 然后就會顯示聊天屏幕骗绕,你可以在聊天屏幕中輸入電子郵件和用戶名藐窄。
如果要查看該應(yīng)用多個用戶之間的通信方式,只需另外打開一個瀏覽器標(biāo)簽頁或窗口酬土,然后導(dǎo)航到“http://localhost:8000”荆忍。 輸入不同的電子郵件和用戶名。然后輪流從兩個窗口發(fā)送消息撤缴,這樣就可以看到多個用戶之間的通信方式了刹枉。
結(jié)論
這只是一個基本的聊天應(yīng)用程序,你可以在此基礎(chǔ)上進(jìn)行更多的改進(jìn)屈呕,添加更多的其他功能微宝,并上傳源代碼,期待你能實現(xiàn)新用戶加入或者離開聊天時的私人提醒或者通知虎眨。盡情創(chuàng)造吧蟋软,此處不設(shè)限!我希望這篇文章對你有所幫助嗽桩,并希望借此啟發(fā)你使用 WebSockets 和 Go 開始創(chuàng)建自己的實時應(yīng)用程序岳守。