golang websocket編程

上篇文章講了TCP/IP的一些基礎概念,並通過go內置的包實現了socket編程。本篇文章來了解一下另外一個概念——WebSocket。但從命名上來看WebSocket和Socket很類似,但是其實兩者並沒有直接的聯繫。Websocket跟HTTP對應,基是於TCP 協議之上的長連接應用層協議。Socket是操作系統抽象出來,方便我們使用TCP/UDP協議編程方法,屬於傳輸層協議。兩者其實沒有直接聯繫,就像Java和JavaScript的關係一樣。

1. 基礎概念

1.1 Websocket定義

WebSocket是一種應用層通信協議,可在單個TCP連接上進行全雙工通信。WebSocket協議在2011年由IETF標準化爲RFC 6455,後由RFC 7936補充規範。Web IDL中的WebSocket API由W3C標準化。

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就可以創建持久性的連接,並進行雙向數據傳輸。

從上面概念上可以看出WebSocket的一些特點:應用層通信協議、基於單個TCP連接、持久連接、全雙工雙向數據傳輸等。這些是比較粗泛的概念,任何一種協議的出現肯定有它的應用場景,下面就簡單介紹一下爲什麼會出現WebSocket這一應用層通信協議。

1.2 爲什麼需要Websocket

客戶端跟服務器之間的交互一般都是使用HTTP協議進行通信的,相比較Websocket,HTTP最大的問題就是Http是個單工的協議,請求只能由客戶端發起,然後服務器收到請求後處理並回傳信息,服務器不能主動向客戶端發送信息。對於一般的場景,HTTP是比較適用的,因爲HTTP是一個單工的短連接協議,可以很好的節省連接維繫的成本,客戶端服務端完成一次交互後,就會斷開TCP連接,以便其它客戶端可以獲取連接,完成跟服務器的交互。但是一旦出現類似於聊天這種場景,HTTP就不是很適用了,爲了實現即時通訊,客戶端必須通過“輪詢”的方式,在特定的時間間隔內,由客戶端對服務器發出 HTTP Request,服務器在收到請求後,返回最新的數據給瀏覽器刷新。可以想到,這種方式是有很多缺點的。

  • 服務器消耗資源大

爲了應付客戶端的輪詢,HTTP連接必須不斷打開關閉,或者一直保持打開的狀態,即使服務器的狀態沒有發生變化,客戶端資源也會一直被佔用,沒有辦法被釋放

  • 效率低下

輪詢中獲得的有效信息不可控,有可能一整天數據才發生一次變化,但是爲了獲得這個變化,卻不得不一整天都在對服務器進行輪詢,效率非常低

  • 浪費帶寬資源

HTTP請求是包含着各種頭部信息的,這就意味着,即使輪詢的請求裏面沒有任何信息,一個HTTP也要有好幾百B的數據量,在高併發的狀況下進行輪詢,流量可能會非常龐大了,而且這些流量都是無用的

雖然比較新的輪詢技術,比如Comet。可以實現雙向通信,但仍然需要反覆發出請求。而且在Comet中普遍採用的HTTP長連接也會消耗服務器資源。基於這種情況,HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬,並且能夠更實時地進行通訊

1.3 WebSocket簡介

WebSocket是一種與HTTP不同的協議,兩者都位於OSI模型的應用層,並且都依賴於傳輸層的TCP協議。雖然它們不同,但RFC 6455規定:“WebSocket設計爲通過80和443端口工作,以及支持HTTP代理和中介”,從而使其與HTTP協議兼容。 爲了實現兼容性,WebSocket握手使用HTTP Upgrade頭從HTTP協議更改爲WebSocket協議。

WebSocket協議支持Web瀏覽器(或其他客戶端應用程序)與Web服務器之間的交互,具有較低的開銷,便於實現客戶端與服務器的實時數據傳輸。 服務器可以通過標準化的方式來實現,而無需客戶端首先請求內容,並允許消息在保持連接打開的同時來回傳遞。通過這種方式,可以在客戶端和服務器之間進行雙向持續對話。 通信通過TCP端口80或443完成,這在防火牆阻止非Web網絡連接的環境下是有益的。

大多數瀏覽器都支持該協議,包括Google Chrome、Firefox、Safari、Microsoft Edge、Internet Explorer和Opera。

與HTTP不同,WebSocket提供全雙工通信。此外,WebSocket還可以在TCP之上啓用消息流。TCP單獨處理字節流,沒有固有的消息概念。 在WebSocket之前,使用Comet可以實現全雙工通信。但是Comet存在TCP握手和HTTP頭的開銷,因此對於小消息來說效率很低。WebSocket協議旨在解決這些問題。

WebSocket協議規範將ws(WebSocket)和wss(WebSocket Secure)定義爲兩個新的統一資源標識符(URI)方案,分別對應明文和加密連接。除了方案名稱和片段ID(不支持#)之外,其餘的URI組件都被定義爲此URI的通用語法。使用瀏覽器開發人員工具,開發人員可以檢查WebSocket握手以及WebSocket框架。

1.4 WebSocket優點

  • 較少的控制開銷。在連接創建後,服務器和客戶端之間交換數據時,用於協議控制的數據包頭部相對較小。在不包含擴展的情況下,對於服務器到客戶端的內容,此頭部大小隻有2至10字節(和數據包長度有關);對於客戶端到服務器的內容,此頭部還需要加上額外的4字節的掩碼。相對於HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了。
  • 更強的實時性。由於協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對於HTTP請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間內更多次地傳遞數據。
  • 保持連接狀態。與HTTP不同的是,Websocket需要先創建連接,這就使得其成爲一種有狀態的協議,之後通信時可以省略部分狀態信息。而HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。
  • 更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕鬆地處理二進制內容。
  • 可以支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。如部分瀏覽器支持壓縮等。
  • 更好的壓縮效果。相對於HTTP壓縮,Websocket在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率。

1.5 WebSocket原理

WebSocket協議比較簡單,在第一次握手通過以後,連接便建立成功,其後的通訊數據都是以”\x00″開頭,以”\xFF”結尾。在客戶端,這個是透明的,WebSocket 組件會自動將原始數據 “掐頭去尾”。

瀏覽器發出WebSocket連接請求,然後服務器發出迴應,然後連接建立成功,這個過程通常稱爲 “握手” (handshaking)。請求和反饋信息如下所示:

在請求中的”Sec-WebSocket-Key”是隨機的,對於整天跟編碼打交道的程序員,一眼就可以看出來:這個是一個經過base64編碼後的數據。服務器端接收到這個請求之後需要把這個字符串連接上一個固定的字符串:

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

即:f7cb4ezEAl6C3wRaU6JORA== 連接上那一串固定字符串,生成一個這樣的字符串:

f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

對該字符串先用sha1安全散列算法計算出二進制的值,然後用base64對其進行編碼,即可以得到握手後的字符串:

rE91AJhfC+6JdVcVXOGJEADEJdQ=

將之作爲響應頭Sec-WebSocket-Accept的值反饋給客戶端。如此操作,可以儘量避免普通HTTP請求被誤認爲Websocket協議。

2. golang實現WebSocket

接下來我們將實現一個簡單的例子:用戶輸入信息,客戶端通過WebSocket將信息發送給服務器端,服務器端收到信息之後主動Push信息到客戶端,然後客戶端將輸出其收到的信息。

2.1 client端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<script type="text/javascript">
    var sock = null;
    var wsuri = "ws://127.0.0.1:1234";

    window.onload = function () {

        console.log("onload");

        sock = new WebSocket(wsuri);

        sock.onopen = function () {
            console.log("connected to " + wsuri);
        }

        sock.onclose = function (e) {
            console.log("connection closed (" + e.code + ")");
        }

        sock.onmessage = function (e) {
            console.log("message received: " + e.data);
        }
    };

    function send() {
        var msg = document.getElementById('message').value;
        sock.send(msg);
    };
</script>
<h1>WebSocket Echo Test</h1>
<form>
    <p>
        Message: <input id="message" type="text" value="Hello, world!">
    </p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

可以看到客戶端JS,很容易的就通過WebSocket函數建立了一個與服務器的連接,當握手成功後,會觸發WebScoket對象的onopen事件,告訴客戶端連接已經成功建立。客戶端一共綁定了四個事件:

  • onopen 建立連接後觸發
  • onmessage 收到消息後觸發
  • onerror 發生錯誤時觸發
  • onclose 關閉連接時觸發

2.2 server端

package main

import (
	"fmt"
	"golang.org/x/net/websocket"
	"log"
	"net/http"
)

func Echo(ws *websocket.Conn) {
	var err error

	for {
		var reply string

		if err = websocket.Message.Receive(ws, &reply); err != nil {
			fmt.Println("Can't receive")
			break
		}

		fmt.Println("Received back from client: " + reply)

		msg := "Received:  " + reply
		fmt.Println("Sending to client: " + msg)

		if err = websocket.Message.Send(ws, msg); err != nil {
			fmt.Println("Can't send")
			break
		}
	}
}

func main() {
	http.Handle("/", websocket.Handler(Echo))

	if err := http.ListenAndServe(":1234", nil); err != nil {
		log.Fatal("ListenAndServe:", err)
	}
}

當客戶端將用戶輸入的信息Send之後,服務器端通過Receive接收到了相應信息,然後通過Send發送了應答信息,服務端console打印信息如下:

Received back from client: Hello, world!
Sending to client: Received:  Hello, world!

客戶端console如下:

客戶端在與服務器端建立連接後,成功接收到服務器端Push回來的消息。注意紅框下面的connection closed,是因爲我手動關閉了服務端服務導致的,並不是因爲在客戶端服務端完成交互後,主動斷開了連接。

通過上面的例子我們看到客戶端和服務器端實現WebSocket非常的方便,Go的源碼net分支中已經實現了這個的協議,我們可以直接拿來用。

參考鏈接:

1. 維基百科——WebSocket

2. 爲什麼不直接使用socket,還要定義一個新的websocket 的呢?

3. 《Go Web編程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章