造輪子 Websocket 現(xiàn)在就 Go
MD: 2019?年?12?月17?日巨坊,??03:45:10
https://github.com/jimboyeah/demo
筆者堅果有幸從事軟件開發(fā)酪术,一直都是興趣驅(qū)動的工作享郊。第一次接觸計算機是 1999 年后的事壮啊,我用來學習的電腦是大哥買來準備學 CAD 的 486 機坎藐,當時 CPU 還是威勝的 333MHz 主頻遇西,硬盤也只有 4GB灭衷,系統(tǒng)是 Windows 98 SE次慢。那時所謂的學電腦純屬拆玩具模式,因為手上可用的資源不多今布,網(wǎng)絡也不發(fā)達经备,也沒有太豐富的參考資料,相關(guān)的圖書也不是太豐富部默。所以翻查硬盤或系統(tǒng)光盤文件成了日城置桑活動,除此之外傅蹂,DOS 游戲也和紅白機具有一樣的可玩性纷闺。彼時,BAT 腳本和 Windows 系統(tǒng)光盤中 QBasic 腳本編程工具成了不錯的好玩具份蝴。后來玩起了 Visual Studio 6.0犁功,主要是 VB 和 VBA 腳本編程,C 語言也開始了解一些婚夫,C++ 幾乎沒有基礎可言浸卦,所以 Visual C++ 一直玩不動 MFC 什么的更是不知云云。當然了案糙,集成開發(fā)工具提供的最大的好處也就體現(xiàn)在這限嫌,即使你是個傻瓜也能毫不費力地運行配置好的模板程序靴庆,編譯成完整的可運行程序。不知不覺怒医,堅果從曾經(jīng)的傻瓜程序員一路走到今天炉抒,沒有興趣帶領(lǐng)還真不會有今天。
[TOC]
內(nèi)容提要
這是我二進 GitChat 的創(chuàng)造稚叹,距離今年 ?6 ?月分?第一次發(fā)布《從 JavaScript 入門到 Vue 組件實踐》大談前端技術(shù)全局觀焰薄、30' JavaScript 入門課,還有 VSCode 和 Sublime 這些好用的開發(fā)工具扒袖。到如今已經(jīng)有近半年時間塞茅,?期間經(jīng)歷了較大的工作變動,技術(shù)上已經(jīng)以腳本后端轉(zhuǎn)到 Golang 為主僚稿,這是一種我一直期待的語言凡桥。期間也學到一些技術(shù)領(lǐng)域比較不容易學習到的知識蟀伸,有項目管理層面的蚀同,有職業(yè)規(guī)劃方面的,對知識付費時代也有了更深入的理解啊掏。
那么 Golang 作為一款以便利的并發(fā)編程的語言蠢络,用在后端的開發(fā)真的是不要太好。
Golang 雖然它已經(jīng)有 10 歲大了迟蜜,最早接觸也是 2012 年左右刹孔,但是真正花心思學起來是今年的 7 月份。Golang 號稱 21 世紀的 C 語言娜睛,這確實是對我最大的吸引力髓霞,它的特點可以總結(jié)為 C + OOP,以松散組合的方式去實現(xiàn)面向?qū)ο蟮木幊趟季S畦戒。完全不像 C++ 把對象數(shù)據(jù)模型設計的異常復雜方库,把一種編程語言搞得自己發(fā)明人都不能完全掌握。當然每種語言都有它的適用領(lǐng)域及特點障斋,免不了一堆人貶低 Golang 沒有泛型之類纵潦,確實 Golang 1.x 就是沒有提供實現(xiàn)。如日中天的 Python 就是個典型垃环,作為奇慢的腳本解析型語言邀层,慢這個缺點完全掩蓋不了它中人工智能算法領(lǐng)域的應用,也完全阻擋不了爬蟲一族賴以為生遂庄。這種取舍其實就是一種效益的體現(xiàn)寥院,選擇恰當?shù)墓ぞ咦鲞m合的事!
我們將從網(wǎng)絡協(xié)議層面來打開 Golang 編程大門涛目,學習關(guān)于 Websocket 網(wǎng)絡協(xié)議的相關(guān)知識點秸谢,在 TCP/IP 協(xié)議棧中经磅,新加入的 Websocket 分量也是重量級的。WebSocket 作為實時性要求較高場合的應用協(xié)議钮追,主要應用在在線網(wǎng)頁聊天室预厌、頁游行業(yè)等等。掌握 Websocket 技能元媚,你值得擁有轧叽!
在本輪學習中,你可以 Get 到技能:
- 如何擁有快速掌握一種計算機語言的能力刊棕;
- 理解幾個基本網(wǎng)絡概念:
- Persistent connection 長連接炭晒;
- Temporary connection 短連接;
- Polling 輪詢甥角;
- LongPolling 長輪詢网严;
- Websocket 核心的數(shù)據(jù)幀 Data Framing 構(gòu)造;
- Websocket 握手連接建立數(shù)據(jù)通訊過程嗤无;
- 實現(xiàn)一個 go-my-websocket 簡約版 Websocket 服務器震束;
- 深入分解 Golang 的 Engine.io 及 Socket.io 的應用;
- 獲得一份完整的電子版 PDF当犯;
- 獲得一份完整的 go-my-websocket 代碼垢村;
- 通過交流活動獲得問題解答機會;
從任天堂紅白機時代接觸單片機嚎卫,盡管那時不懂卻被深深吸引了嘉栓;從 MS-DOS 時代結(jié)緣計算機,就這樣一路披荊斬棘前行拓诸;很多人說 IT 人是吃青春飯的侵佃,對于我,一個 80 后奠支,在乳臭未干的時候就聞到這飯香了馋辈,到現(xiàn)在也沒覺得吃夠吃厭倦了。我只當這是個興趣胚宦,現(xiàn)在這個愛好還能給我?guī)硪环菔杖攵选?/p>
by Jeangowhy 微信同名(jimboyeah?gmail.com)
Tue Dec 17 2019 04:23:08 GMT+0800 (深圳寶安)
websocket
The WebSocket Protocol https://tools.ietf.org/html/rfc6455
WebSocket vs Polling
先理解幾個概念
- Persistent connection 長連接
- Temporary connection 短連接
- Polling 輪詢
- LongPolling 長輪詢
建立 TCP 連接后首有,在數(shù)據(jù)傳輸完成時還保持 TCP 連接不斷開,不發(fā)RST包枢劝、不進行四次握手斷開井联,并等待對方繼續(xù)用這個 TCP 通道傳輸數(shù)據(jù),相反的就是短連接您旁。通常 HTTP 連接就是短連接烙常,瀏覽器建立 TCP 連接請求頁面,服務器發(fā)送數(shù)據(jù),然后關(guān)閉連接蚕脏。下次再需要請求數(shù)據(jù)時又重新建立 TCP 連接侦副,一問一答是短連接的一個特點。而新的 HTTP 2.0 規(guī)范中驼鞭,為了提高性能則使用了長連接來復用同一個 TCP 連接來傳送不同的文件數(shù)據(jù)秦驯。
參考 RFC 2616 HTTP 1.1 規(guī)范文檔關(guān)于 Persistent connection 的部分 https://tools.ietf.org/html/rfc2616#page-44
HTTP 頭信息 Connection: Keep-alive 是 HTTP 1.0 瀏覽器和服務器的實驗性擴展,當前的 HTTP 1.1 RFC 2616 文檔沒有對它做說明挣棕,因為它所需要的功能已經(jīng)默認開啟译隘,無須帶著它,但是實踐中可以發(fā)現(xiàn)洛心,瀏覽器的報文請求都會帶上它固耘。如果不希望使用長連接,則要在請求報文首部加上 Connection: close词身。
《HTTP權(quán)威指南》提到厅目,有部分古老的 HTTP 1.0 代理不理解 Keep-alive,而導致長連接失效:客戶端-->代理-->服務端法严,客戶端帶有 Keep-alive损敷,而代理不認識,于是將報文原封不動轉(zhuǎn)給了服務端渐夸,服務端響應了 Keep-alive嗤锉,也被代理轉(zhuǎn)發(fā)給了客戶端,于是保持了 客戶端-->代理
連接和 代理-->服務端
連接不關(guān)閉墓塌,但是,當客戶端第發(fā)送第二次請求時奥额,代理會認為當前連接不會有請求了苫幢,于是忽略了它,長連接失效垫挨。書上也介紹了解決方案:遇到 HTTP 1.0 就忽略 Keep-alive韩肝,客戶端就知道當前不該使用長連接。
使用了HTTP長連接(HTTP persistent connection )之后的好處九榔,包括可以使用HTTP 流水線技術(shù)(HTTP pipelining哀峻,也有翻譯為管道化連接),它是指哲泊,在一個TCP連接內(nèi)剩蟀,多個HTTP請求可以并行,下一個HTTP請求在上一個HTTP請求的應答完成之前就發(fā)起切威。
Client 和 Server 間的實時數(shù)據(jù)傳輸是一個很重要的需求育特,但早期 HTTP 只能通過 AJAX 輪詢 Pooling 方式實現(xiàn),客戶端定時向服務器發(fā)送 Ajax 請求先朦,服務器接到請求后馬上返回響應信息并關(guān)閉連接缰冤,這就時短連接的應用犬缨。輪詢帶來以下問題:
- 服務器必須為同一個客戶端的輪詢請求建立不同的 TCP 連接,算上 TCP 的三握手過程棉浸,每個 HTTP 連接的建立就需要來回通訊將近 10 次怀薛;
- 客戶端腳本需要維護出站/入站連接的映射,即管理本地請求與服務器響應的對應關(guān)系迷郑;
- 請求中有大半是無用乾戏,每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭信息,浪費帶寬和服務器資源三热。
長輪詢 LongPolling 是基于長連接實現(xiàn)的鼓择,是對 Polling 的一種改進。Client 發(fā)送請求就漾,此時 Server 可以發(fā)送數(shù)據(jù)或等待數(shù)據(jù)準備好:
- 如果 Server 有新的數(shù)據(jù)需要傳送呐能,就立即把數(shù)據(jù)發(fā)回給 Client,收到數(shù)據(jù)后又立即再發(fā)送 HTTP 請求抑堡。
- 如果 Server 沒有新數(shù)據(jù)需要傳送摆出,與 Polling 的方式不同的是,Server 不是立即發(fā)送回應給 Client首妖,而是將這個請求保持住偎漫,等待有新的數(shù)據(jù)來到,再去響應這個請求有缆。
- 如果 Server 長時沒有數(shù)據(jù)響應象踊,這個 HTTP 請求就會超時,Client 收到超時信息后棚壁,重新向服務器發(fā)送一個 HTTP 請求杯矩,循環(huán)這個過程。
LongPolling 的方式雖然在某種程度上減少了網(wǎng)絡帶寬和 CPU 利用率等問題袖外,但仍存在缺陷史隆,因為 LongPolling 還是基于一問一答的 HTTP 協(xié)議模式。當 Server 的數(shù)據(jù)更新速度較快曼验,Server 在傳送一個數(shù)據(jù)包給 Client 后必須等待下一個 HTTP 請求到來泌射,才能傳遞第二個更新的數(shù)據(jù)包給 Browser,這種場景在 HTTP 上實現(xiàn)的實時聊天幾多人游戲是常見的鬓照。這樣的話熔酷,Browser 顯示實時數(shù)據(jù)最快的時間為 2 倍 RTT 往返時間。還不考慮網(wǎng)絡擁堵的情況颖杏,這個應該是不能讓用戶接受的纯陨。另外,由于 HTTP 數(shù)據(jù)包的頭部數(shù)據(jù)量很大,通常有 400 多個字節(jié)翼抠,但真正被服務器需要的數(shù)據(jù)卻很少咙轩,可能只有 10個字節(jié)左右,這樣的數(shù)據(jù)包在網(wǎng)絡上周期性傳輸阴颖,難免對網(wǎng)絡帶寬是一種浪費活喊。
WebSocket 正是基于支持客戶端和服務端的雙向通信、簡化協(xié)議頭這些需求下量愧,基于 HTTP 和 TCP 的基礎上登上了 Web 的舞臺钾菊。由于 HTTP 協(xié)議的原設計不是用來做雙向通訊的,它只是一問一答的執(zhí)行偎肃,客戶端發(fā)送請求煞烫,服務器進行答復,客服端需要什么文件累颂,服務器就提供文件數(shù)據(jù)滞详。
WebSocket 通信協(xié)議是一種雙向通信協(xié)議,在建立連接后紊馏,WebSocket 服務器和 Client 都能主動的向?qū)ο蟀l(fā)送或接收數(shù)據(jù)料饥,就像 Socket 一樣,所以建立在 Web 基礎上的 WebSocket 需要通過升級 HTTP 連接來實現(xiàn)類似 TCP 那樣的握手連接朱监,連接成功后才能相互通信岸啡。相互溝通的 Header 很小,大概只有 2Bytes赫编。服務器不再被動的接收到瀏覽器的請求之后才返回數(shù)據(jù)巡蘸,而是在有新數(shù)據(jù)時就主動推送給瀏覽器。
WebSocket 協(xié)議本質(zhì)上是一個基于 TCP 的協(xié)議沛慢。為了建立一個 WebSocket 連接赡若,客戶端瀏覽器首先要向服務器發(fā)起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同团甲,包含了一些附加頭信息,其中附加頭信息 Upgrade: WebSocket
表明這是一個申請協(xié)議升級的 HTTP 請求黍聂,服務器端解析這些附加的頭信息然后產(chǎn)生應答信息返回給客戶端躺苦,客戶端和服務器端的 WebSocket 連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞信息产还,并且這個連接會持續(xù)存在直到客戶端或者服務器端的某一方主動的關(guān)閉連接匹厘。
因為 WebSocket 是一種新的通信協(xié)議,目前還是草案,沒有成為標準,市場上也有成熟的實現(xiàn) WebSocket 協(xié)議開源 Library 可供使用妄田。例如 PyWebSocket蒋搜、 WebSocket-Node荤胁、 LibWebSockets 等船逮。
本文介紹內(nèi)容大致如下:
- Websocket 握手機制細節(jié)
- Websocket 數(shù)據(jù)幀結(jié)構(gòu)
Websocket 協(xié)議通信分為兩個部分薇搁,先是握手跪削,再是數(shù)據(jù)傳輸匕累。 主要是建立連接握手 Opening Handshake陵刹,斷開連接握手 Closing Handshake 則簡單地利用了 TCP closing handshake (FIN/ACK)。
如下就是一個基本的 Websocket 握手的請求與回包:
Open handshake 請求
GET /chat HTTP/1.1
Host: server.example.com
Origin: http://example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Open handshake 響應
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Websocket 需要使用到的附加信息頭主要有以下幾個:
- Sec-WebSocket-Key
- Sec-WebSocket-Extensions 客戶端查詢服務端是否支持指定的擴展特性
- Sec-WebSocket-Accept 客戶端認證
- Sec-WebSocket-Protocol 子協(xié)議查詢
- Sec-WebSocket-Version 協(xié)議版本號
Websocket協(xié)議中如何確被逗伲客戶端與服務端接收到握手請求呢衰琐? 這里就要說到HTTP的兩個頭部字段,Sec-Websocket-Key
與 Sec-Websocket-Accept
炼蹦。
-
首先客戶端發(fā)起請求羡宙,在頭部
Sec-Websocket-Key
中隨機生成 base64 字符串;Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
-
服務端收到請求后掐隐,根據(jù)頭部
Sec-Websocket-Key
與約定的 GUID, [RFC4122])258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接狗热;dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
-
使用 SHA-1 算法得到拼接的字符串的摘要 hash,最后用 base64 編碼放入頭部
Sec-Websocket-Accept
返回客戶端做認證瑟枫。SHA1= b37a4f2cc0624f1690f64606cf385945b2bec4ea Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
更詳細的說明可以看 RFC 6455 文檔斗搞。
Data Framing 數(shù)據(jù)幀
根據(jù) RFC 6455 定義,websocket 消息統(tǒng)稱為 messages慷妙,可以由多個幀 frame 構(gòu)成僻焚。有文本數(shù)據(jù)幀,二進制數(shù)據(jù)幀膝擂,控制幀三種架馋,Websocket 官方定義有 6 種類型并預留了 10 種類型用于未來的擴展。
了解完 websocket 握手的大致過程后叉寂,這個部分介紹下 Websocket 數(shù)據(jù)幀與分片傳輸?shù)姆绞剑饕^部信息是前面的 2 byte屏鳍。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| and more continued |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
FIN: 1 bit 表示是否為分片消息 fragment 的最后一個數(shù)據(jù)幀的標記位,第一幀也可能是最后一幀钓瞭。
RSV1, RSV2, RSV3: 保留,都是 1 bit堤结。
-
opcode: 表示傳輸?shù)?Payload 數(shù)據(jù)格式竞穷,如1表示純文本(utf8)數(shù)據(jù)幀,2表示二進制數(shù)據(jù)幀
%x0 denotes a continuation frame %x1 denotes a text frame %x2 denotes a binary frame %x3-7 are reserved for further non-control frames %x8 denotes a connection close %x9 denotes a ping %xA denotes a pong %xB-F are reserved for further control frames
MASK: 表示 Payload 數(shù)據(jù)是否進行標記運算 client-to-server masking妒蔚,和 Masking-key 相關(guān)肴盏。
-
Payload length: 傳輸數(shù)據(jù)內(nèi)容的總長度菜皂。
可以是 7 bits, 7+16 bits, or 7+64 bits厉萝,后兩種對應條件是 Payload len == 126/127谴垫,即分別增加翩剪。
載荷數(shù)據(jù) Payload data 長度為 0-125 字節(jié)時乳怎,他就是 payload length 總載荷長度。
如果 Payload len = 126前弯,那么后續(xù)的 2 bytes 無符號整數(shù)表示 payload length恕出;
如果 Payload len = 127浙巫,那么后續(xù)的 8 bytes 無符號整數(shù)作為 payload length 但最高有效位必須為 0的畴。 -
Masking-key: 0 or 4 bytes
MASK==1 時用 4 bytes 表示 Masking-key苗傅,所有從 client 發(fā)送到 server 的數(shù)據(jù)需要與 Masking-key 進行異或操作渣慕,防止一些惡意程序直接獲取傳輸內(nèi)容內(nèi)容逊桦。
Masking-key 由客戶端隨機生成 32-bit 值强经,不能被預測匿情,[RFC4086] 討論了關(guān)于安全敏感應用熵的來源
[RFC4086] discusses what entails a suitable source of entropy for security-sensitive applications.
要將標記過的 Payload data 轉(zhuǎn)換回來炬称,或者對數(shù)據(jù)進行標記的算法是統(tǒng)一的玲躯,并且不用考慮轉(zhuǎn)換數(shù)據(jù)的方向跷车。第 i 的標記數(shù)據(jù)由第 i 位的原始數(shù)據(jù) XOR Masking-Key[i % 4] 得到朽缴。
Octet i of the transformed data ("transformed-octet-i") is the XOR of octet i of the original data ("original-octet-i") with octet at index i modulo 4 of the masking key ("masking-key-octet-j"): j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
注意數(shù)據(jù)幀的 Payload length 是指用戶數(shù)據(jù)長度即 Masking-key 后面的數(shù)據(jù)長度不铆,并不含 Masking-key 的長度誓斥。
-
Payload data: (x+y) bytes
載荷數(shù)據(jù) Payload data 等于擴展數(shù)據(jù) Extension data 與 Application data 的總和劳坑。
一般 Extension data 為 0 字節(jié)距芬,除非 Payload len 指定 126/127 這兩個值框仔,并且數(shù)據(jù)長度需要在 opening handshake 握手階段協(xié)商確定离斩。
Application data 會占據(jù) Extension data 后面的位置,長度是 Payload length 減掉 Extension data 的長度棋弥。
當一個完整消息體大小不可知時顽染,Websocket 支持分片傳輸 Fragmentation粉寞。這樣可以方便服務端使用可控大小的 buffer 來傳輸分段數(shù)據(jù)藏澳,減少帶寬壓力翔悠,同時可以有效控制服務器內(nèi)存蓄愁。
同時在多路傳輸?shù)膱鼍跋麓樽ィ梢岳梅制夹g(shù)使不同的 namespace 的數(shù)據(jù)能共享對外傳輸通道丹拯。不用等待某個大的 message 傳輸完成乖酬,進入等待狀態(tài)咬像。
對于控制數(shù)據(jù)幀 Control Frames 不能使用分片方式肮柜,并且 Playload 數(shù)據(jù)不大于 125 bytes审洞,但可以在分片幀中插隊傳輸预明。通過 opcodes 最高位置位來標記控制幀撰糠,0x8 (Close), 0x9 (Ping), 0xA (Pong)阅酪,Opcodes 0xB-0xF 保留术辐。
fragmentation 分片規(guī)則:
無分片消息作為單幀 single frame 傳輸辉词,F(xiàn)IN 置位且 opcode 不為 0瑞躺。
-
分片消息 fragmented message 包括單幀的分片消息 FIN 清位且 opcode 不為 0幢哨,后續(xù)任意 opcode 為 0 的幀捞镰,再跟 FIN=1 opcode=0 的結(jié)束幀岸售。分片消息作為一個消息整體凸丸,
EXAMPLE: 對于一個三分片的 text message - the first fragment opcode = 0x1 and a FIN bit clear; - the second fragment opcode = 0x0 and a FIN bit clear; - the third fragment opcode = 0x0 and a FIN bit that is set.
控制幀 Control frames 可以再分片中插隊傳輸甲雅,單本身本能作為分片方式傳輸抛人。
消息分片 Message fragments 必須按發(fā)送方的順序傳送給接收方妖枚。
兩條分片消息不能交錯傳遞绝页,除非已經(jīng)協(xié)商好對應的擴展處理莱没。
端點必須能處理分片消息中間插入的控制幀饰躲。
擴展特性支持 Extensibility嘹裂,Websocket 協(xié)議設計考慮了擴展特性支持寄狼,通過 Sec-WebSocket-Extensions 來協(xié)商需要支持的特性泊愧,客戶端需要服務器支持的擴展特性添加到這個信息頭中拼卵。服務端接收到后進行確認腋腮,如果支持某個特性即寡,就通過這個信息頭返回告訴客服端是支持的特性聪富。常用的有信息壓縮擴展有消息壓縮 permessage-deflate,對消息進行壓縮奸披,deflate 就是給氣球放氣的意思阵面,也是壓縮算法名稱样刷,表示壓縮很形象置鼻。如果這個消息需要分片發(fā)送沃疮,那么就對壓縮過的數(shù)據(jù)進行分割發(fā)送,接收時先拼接在解壓縮俊啼。
比如客戶端查詢時發(fā)送信息頭如下授帕,表示和服務器協(xié)商一下壓縮擴展:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
那么服務器如果支持消息壓縮擴展功能跛十,那么協(xié)商返回結(jié)果如下:
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover
permessage-deflate 有四個配置參數(shù)芥映,配置兩端的兩個工作方式 no_context_takeover
、 max_window_bits
惊来。后者配置 DEFLATE 壓縮算法的滑動窗口參數(shù)裁蚁,參考 RFC 7692 關(guān)于術(shù)語 LZ77 sliding window:
- server_no_context_takeover
- client_no_context_takeover
- server_max_window_bits
- client_max_window_bits
以下是關(guān)于擴展特性的不規(guī)范也不完整可能的應用建議:
Below are some anticipated uses of extensions. This list is neither complete nor prescriptive.
- o "Extension data" may be placed in the "Payload data" before the "Application data".
- o Reserved bits can be allocated for per-frame needs.
- o Reserved opcode values can be defined.
- o Reserved bits can be allocated to the opcode field if more opcode values are needed.
- o A reserved bit or an "extension" opcode can be defined that allocates additional bits out of the "Payload data" to define larger opcodes or more per-frame bits.
關(guān)于壓縮算法相關(guān)的參考材料:
- RFC 1950: ZLIB compressed data format specification version 3 https://tools.ietf.org/html/rfc1950
- RFC 1951 DEFLATE Compressed Data Format Specification ver 1.3 https://tools.ietf.org/html/rfc1951
- RFC 7692 - Compression Extensions for WebSocket https://tools.ietf.org/html/rfc7692
完整的 Websocket 實現(xiàn)可以參考 Gorilla websocket 包原代碼。
Data Frame Examples
-
A single-frame unmasked text message
* 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")
-
A single-frame masked text message
* 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains "Hello")
-
A fragmented unmasked text message
* 0x01 0x03 0x48 0x65 0x6c (contains "Hel") * 0x80 0x02 0x6c 0x6f (contains "lo")
-
A single-frame masked close message with 1001 close code
- 8882 8976 8DDA 8A9F
- 8882 C831 8FE2 CBD8
-
Two masked frames, the first one is Socket.io ping message, and close message at the end with no close codde
- C183 4C93 383A 7E9138 8880 A34E A2C4
-
Unmasked Ping request and masked Ping response
* 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of "Hello", but the contents of the body are arbitrary) * 0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains a body of "Hello", matching the body of the ping)
-
256 bytes binary message in a single unmasked frame
* 0x82 0x7E 0x0100 [256 bytes of binary data]
-
64KiB binary message in a single unmasked frame
* 0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]
telnet 工具的使用
TCP 網(wǎng)絡世界,一切皆Socket舞萄!事實也是倒脓,現(xiàn)在的網(wǎng)絡編程幾乎都是用的socket崎弃。
Telnet 是一個標準的 TCP 協(xié)議應用程序线婚,通過它可以建立 TCP 連接塞弊,然后發(fā)送制符串內(nèi)容到服務器游沿,這樣就可以利用它來模塊 HTTP 的數(shù)據(jù)傳輸诀黍。類似的郵件協(xié)議、 FTP 也能模擬的咒精。
用 Telnet 來模擬 HTTP 請求模叙,首先需要了解 HTTP 請求的數(shù)據(jù)結(jié)構(gòu)范咨,為了簡化只采用最簡單的 HTTP 協(xié)議頭渠啊,只有 GET 動作及 URL 路徑:
GET /chat/telnet HTTP/1.1
Host: localhost
那么現(xiàn)在就執(zhí)行 telnet 連接到服務器,以連接 localhost 為例躲查,一旦連接后所有敲到命令界面的字符都會實時發(fā)送到服務器的接收緩存區(qū)中镣煮,包括回車符也是典唇。在命令界面中將以上內(nèi)容敲進去介衔,回車兩次表示 HTTP 頭部的結(jié)束標志与纽,接著等待服務器回復,也可以先復制好這些 HTTP 協(xié)議頭信息粘貼到 telnet 命令界面中:
telnet localhost 8000
GET /socket.io/ HTTP/1.1
Host: localhost
Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: HRaJmux1hUnhnw4iNlYCYA==
Sec-WebSocket-Version: 13
Upgrade: websocket
telnet localhost 8000
GET /chat/telnet HTTP/1.1
Host: localhost
利用快捷鍵 Ctrl+]
打開命令控制界面蹦肴,這樣就可以通過工具主界面來操作各中選項,也可以看見輸入的內(nèi)容:
GET /IO HTTP/1.1ft Telnet Client
HOST: localhost
Escape 字符為 'CTRL+]'
Microsoft Telnet> ?
命令可能是縮寫矛双。支持的命令為:
c - close 關(guān)閉當前連接
d - display 顯示操作參數(shù)
o - open hostname [port] 連接到主機(默認端口 23)议忽。
q - quit 退出 telnet
set - set 設置選項(鍵入 'set ?' 獲得列表)
sen - send 將字符串發(fā)送到服務器
st - status 打印狀態(tài)信息
u - unset 解除設置選項(鍵入 'set ?' 獲得列表)
?/h - help 打印幫助信息
Microsoft Telnet> set ?
bsasdel 發(fā)送 Backspace 鍵作為刪除
crlf 換行模式 - 引起 return 鍵發(fā)送 CR 和 LF
delasbs 發(fā)送 Delete 鍵作為退格
escape x x 是進入 telnet 客戶端提示的 escape 字符
localecho 打開 localecho
logfile x x 是當前客戶端的日志文件
logging 啟用日志
mode x x 是控制臺或流
ntlm 啟用 NTLM 身份驗證
term x x 是 ansi、vt100速址、vt52 或 vtnt
在 Telnet 控制界面直接回車回到交互界面,或者使用 send 命令發(fā)送數(shù)據(jù)并炮,多按幾次回車就好:
Microsoft Telnet> send apple
發(fā)送字符串 apple
Websocket Demo 握手及數(shù)據(jù)幀的收發(fā)
- engine.io-protocol https://github.com/socketio/engine.io-protocol
- socket.io-protocol https://github.com/socketio/socket.io-protocol
可以使用輔助調(diào)試工具 Fiddler 來抓取 Websocket 數(shù)據(jù)包用于調(diào)試分析羡棵,在主界面的連接列表中雙擊 WS 標記的連接即可在右側(cè)數(shù)據(jù)面板中看到 Websocket 連接傳輸?shù)臄?shù)據(jù)皂冰。
為了簡化秃流,使用 MASK舶胀,約定載荷最大為 125 字節(jié),不使用分包發(fā)送轩端,不使用壓縮擴展基茵,即不返回 Sec-WebSocket-Extensions 的壓縮相關(guān)擴展如 permessage-deflate。
以 Golang 為例根灯,結(jié)合 engine.io-protocol箱吕、 socket.io-protocol 來實現(xiàn)一個簡化版 Websocket 服務,參考 gorilla websocket 的實現(xiàn):
go get github.com/gorilla/websocket
Gorilla WebSocket compared with other packages
Features | github.com/gorilla | golang.org/x/net |
---|---|---|
Passes Autobahn Test Suite | Yes | No |
Receive fragmented message | Yes | No, see note 1 |
Send close message | Yes | No |
Send pings and receive pongs | Yes | No |
Get the type of a received data message | Yes | Yes, see note 2 |
Compression Extensions | Experimental | No |
Read message using io.Reader | Yes | No, see note 3 |
Write message using io.WriteCloser | Yes | No, see note 3 |
Notes:
- Large messages are fragmented in Chrome's new WebSocket implementation.
- The application can get the type of a received data message by implementing
a Codec marshal
function. - The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
Read returns when the input buffer is full or a frame boundary is
encountered. Each call to Write sends a single frame message. The Gorilla
io.Reader and io.WriteCloser operate on a single WebSocket message.
簡易服務器借用了 Gorilla Websocket 的前端示例丽猬,如果需要測試 Engine.io 或 Socket.io 通訊,請按《在 Go 中使用 Socket.IO》文章里的頁面提供的代碼進行修改使用
《在 Go 中使用 Socket.IO》 http://www.reibang.com/p/566a0e2456a9
package main
import (
_ "./example"
"bufio"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
stdLog "log"
"net"
"net/http"
"os"
"runtime"
"time"
)
const (
TOO_LONG = "[payload too long]"
PingTimeout = time.Second * 5
// The message types are defined in RFC 6455, section 11.8.
BrokenMessage MessageType = 0xf0
ContinuationFrame MessageType = 0
TextMessage MessageType = 1
BinaryMessage MessageType = 2
CloseMessage MessageType = 8
PingMessage MessageType = 9
PongMessage MessageType = 10
// Close codes defined in RFC 6455, section 11.7.
CloseNormalClosure MessageCode = 1000
CloseGoingAway MessageCode = 1001
CloseProtocolError MessageCode = 1002
CloseUnsupportedData MessageCode = 1003
CloseNoStatusReceived MessageCode = 1005
CloseAbnormalClosure MessageCode = 1006
CloseInvalidFramePayloadData MessageCode = 1007
ClosePolicyViolation MessageCode = 1008
CloseMessageTooBig MessageCode = 1009
CloseMandatoryExtension MessageCode = 1010
CloseInternalServerErr MessageCode = 1011
CloseServiceRestart MessageCode = 1012
CloseTryAgainLater MessageCode = 1013
CloseTLSHandshake MessageCode = 1015
EIO_Close EngineioPacket = "1"
EIO_Ping EngineioPacket = "2"
EIO_Pond EngineioPacket = "3"
EIO_Message EngineioPacket = "4"
EIO_Upgrade EngineioPacket = "5"
EIO_Noop EngineioPacket = "6"
SIO_Connect SocketioPacket = "0"
SIO_Disconnect SocketioPacket = "1"
SIO_Event SocketioPacket = "2"
SIO_Ack SocketioPacket = "3"
SIO_Error SocketioPacket = "4"
SIO_Binary_Event SocketioPacket = "5"
SIO_Binary_Ack SocketioPacket = "6"
// ErrorCode
ErrorNil ErrorCode = iota
ErrorLength
ErrorKeyLength
ErrorMessageType
)
type MessageType byte
func (s MessageType) String() string {
cs := map[MessageType]string{
0xf0: "BrokenMessage",
0: "ContinuationFrame",
1: "TextMessage",
2: "BinaryMessage",
8: "CloseMessage",
9: "PingMessage",
10: "PongMessage",
}[s]
if cs > "" {
return cs
} else {
return fmt.Sprintf("<MessageType v:%d>", s)
}
}
type EngineioPacket string
func (s EngineioPacket) String() string {
cs := map[EngineioPacket]string{
"1": "EIO_Close",
"2": "EIO_Ping",
"3": "EIO_Pond",
"4": "EIO_Message",
"5": "EIO_Upgrade",
"6": "EIO_Noop",
}[s]
if cs > "" {
return cs
} else {
return fmt.Sprintf("<EngineioPacket v:%q>", s)
}
}
type SocketioPacket string
func (s SocketioPacket) String() string {
cs := map[SocketioPacket]string{
"0": "SIO_Connect",
"1": "SIO_Disconnect",
"2": "SIO_Event",
"3": "SIO_Ack",
"4": "SIO_Error",
"5": "SIO_Binary_Event",
"6": "SIO_Binary_Ack",
}[s]
if cs > "" {
return cs
} else {
return fmt.Sprintf("<SocketioPacket v:%q>", s)
}
}
type MessageCode uint
func (s MessageCode) String() string {
cs := map[MessageCode]string{
1000: "CloseNormalClosure",
1001: "CloseGoingAway",
1002: "CloseProtocolError",
1003: "CloseUnsupportedData",
1005: "CloseNoStatusReceived",
1006: "CloseAbnormalClosure",
1007: "CloseInvalidFramePayloadData",
1008: "ClosePolicyViolation",
1009: "CloseMessageTooBig",
1010: "CloseMandatoryExtension",
1011: "CloseInternalServerErr",
1012: "CloseServiceRestart",
1013: "CloseTryAgainLater",
1015: "CloseTLSHandshake",
}[s]
if cs != "" {
return cs
} else {
return fmt.Sprintf("<MessageCode v:%d>", s)
}
}
type ErrorCode uint
func (s ErrorCode) String() string {
cs := map[ErrorCode]string{
ErrorNil: "ErrorNil",
ErrorLength: "ErrorLength",
ErrorKeyLength: "ErrorKeyLength",
ErrorMessageType: "ErrorMessageType",
}[s]
if cs != "" {
return cs
} else {
return fmt.Sprintf("<ErrorCode v:%d>", s)
}
}
var log = stdLog.New(os.Stderr, "[ws]", stdLog.Ltime)
func main() {
// example.protobuf_test()
// example.Socket_Run()
staticweb := http.StripPrefix("/web/", http.FileServer(http.Dir("./")))
http.Handle("/socket.io/", wsdemo{})
http.Handle("/web/", staticweb)
http.HandleFunc("/", home)
log.SetPrefix("[wsDemo]")
log.Println("Serving at localhost:8000...")
log.Fatal(http.ListenAndServe(":8000", nil))
}
func home(w http.ResponseWriter, r *http.Request) {
homeTemplate.Execute(w, "ws://"+r.Host+"/socket.io/")
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
/*
RFC 6455 Data Framing 數(shù)據(jù)幀
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| and more continued |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
*/
type FrameParser struct {
Raw []byte
parsed bool
Final bool
Rsv1 bool
Rsv2 bool
Rsv3 bool
Opcode byte
Maskingkey []byte
Masking bool
Type MessageType
Code MessageCode
ExHeaderSize uint
Length uint64
Header []byte
Data []byte
}
func (c *FrameParser) Detect(header []byte) (msgtype MessageType, exsize uint, err ErrorCode) {
c.Reset() // ready for a new frame
if len(header) != 2 {
return BrokenMessage, 0, ErrorLength
}
// 2bytes header
c.Final = (header[0] | byte(1<<7)) > 0
c.Rsv1 = (header[0] | byte(1<<6)) > 0
c.Rsv2 = (header[0] | byte(1<<5)) > 0
c.Rsv3 = (header[0] | byte(1<<4)) > 0
c.Opcode = (header[0] & 0x0F)
c.Type = MessageType(c.Opcode)
c.Masking = (header[1] & byte(1<<7)) > 0
c.Length = uint64(header[1] & 0x7f)
c.ExHeaderSize = 0
if c.Masking {
c.ExHeaderSize += 4
}
if c.Length == 126 {
c.ExHeaderSize += 2
} else if c.Length == 127 {
c.ExHeaderSize += 8
}
c.Raw = append([]byte{}, header...) // copy
return c.Type, c.ExHeaderSize, ErrorNil
}
func (c *FrameParser) DecideLength(ex []byte) (payloadlen uint64, err ErrorCode) {
if uint(len(ex)) != c.ExHeaderSize {
return 0, ErrorLength
}
if c.Masking {
c.Maskingkey = ex[len(ex)-4:]
}
if c.Length == 126 {
c.Length = binary.BigEndian.Uint64(ex[0:2])
} else if c.Length == 127 {
c.Length = binary.BigEndian.Uint64(ex[0:8])
}
c.Raw = append(c.Raw, ex...) // copy
return c.Length, ErrorNil
}
func (c *FrameParser) Load(data []byte) ErrorCode {
if uint64(len(data)) != c.Length {
return ErrorLength
}
c.Raw = append(c.Raw, data...) // copy
c.Data = append([]byte{}, data...) // copy
c.Mask()
return ErrorNil
}
func (c *FrameParser) Mask() ErrorCode {
ldata := uint64(len(c.Data))
lkey := len(c.Maskingkey)
if !c.Masking && ldata == c.Length {
return ErrorNil
} else if !c.Masking && ldata != c.Length {
return ErrorLength
} else if c.Masking && lkey != 4 {
return ErrorKeyLength
} else if c.Masking {
for i := uint64(0); i < c.Length; i++ {
j := i % uint64(lkey)
b := c.Maskingkey[j] ^ c.Data[i]
c.Data[i] = b
}
}
return ErrorNil
}
func (c *FrameParser) Reset() {
c.Raw = []byte{}
c.parsed = false
c.Final = false
c.Rsv1 = false
c.Rsv2 = false
c.Rsv3 = false
c.Opcode = 0
c.Maskingkey = []byte{}
c.Masking = false
c.Type = 0
c.Code = 0
c.Length = 0
c.Header = []byte{}
c.Data = []byte{}
}
type wsdemo struct {
w http.ResponseWriter
r *http.Request
netconn net.Conn
brw *bufio.ReadWriter
who string
}
func (c wsdemo) read() {
fm := FrameParser{}
for {
p, err := c.peek(2)
if err != nil {
break
} else if len(p) != 2 {
continue
}
log.Printf("Read a 2bytes header: %d [%x] [%x]\n", len(p), p, p)
// onData...
msgtype, exsize, ec := fm.Detect(p)
if ec != ErrorNil {
continue
}
ex, err := c.peek(int(exsize))
if err != nil {
break
} else if uint(len(ex)) != exsize {
continue
}
log.Printf("Read an ex-header [%d] %s: %d [%x] [%x]\n", exsize, msgtype.String(), len(ex), ex, ex)
payloadlen, _ := fm.DecideLength(ex)
payload, err := c.peek(int(payloadlen))
if err != nil {
break
}
log.Printf("Read payload [%d] %s: %d [%x]", payloadlen, msgtype.String(), len(payload), payload)
fm.Load(payload)
c.onMessage(fm)
}
}
func (c wsdemo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.w = w
c.r = r
if r.URL.RequestURI() == "/socket.io/socket.io.js" {
w.Write([]byte(`
// socket.io.js not provide here, use CDN below:
// https://cdnjs.com/libraries/socket.io
// https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js
`))
return
}
netConn, brw, _ := c.Upgrade(w, r)
c.netconn = netConn
c.brw = brw
c.who = c.netconn.RemoteAddr().String()
log.Println("wsdemo.ServeHTTP()...", r.URL.RequestURI(), r.RequestURI)
Upgrade := r.Header.Get("Upgrade") == "websocket"
Connection := r.Header.Get("Connection") == "Upgrade"
if isWebsocketUpgrade := Upgrade && Connection; isWebsocketUpgrade {
c.OpenHandshake()
c.EngineioOpen("Engine.io Open")
go c.read()
} else {
// w.WriteHeader(200)
// w.Write([]byte("Ok"))
// brw.Write([]byte("Totally-Control: Yes\r\n"))
// brw.Flush()
netConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\nThis is an websocket server, use websocket client to connect."))
netConn.Close()
}
}
func (c wsdemo) peek(size int) (p []byte, err error) {
if size == 0 {
return []byte{}, nil
}
c.netconn.SetDeadline(time.Now().Add(30000 * time.Millisecond))
var brw *bufio.ReadWriter = c.brw
buf, err := brw.Peek(size)
brw.Discard(len(buf))
// buf := []byte{}
// size, err := brw.Read(buf)
if err != nil && err == io.EOF { // client disconnect
c.netconn.Close()
return buf, err
} else if err != nil {
switch err.(type) {
case *net.OpError:
var e *net.OpError = err.(*net.OpError)
log.Printf("Peeking net.OpError [%x] timeout:%t temporary:%t %#+v", p, e.Timeout(), e.Temporary(), e.Err)
if !(e.Timeout() || e.Temporary()) {
log.Printf("Client leave %s", c.who)
c.netconn.Close()
return buf, err
}
default:
log.Printf("Peeking error %#+v", err)
}
}
return buf, nil
}
func (c wsdemo) read_demo() {
var brw *bufio.ReadWriter = c.brw
for {
c.netconn.SetDeadline(time.Now().Add(30000 * time.Millisecond))
// size := 2
// buf, err := brw.Peek(size)
// brw.Discard(len(buf))
// buf := []byte{}
// size, err := brw.Read(buf)
buf, ok, err := brw.ReadLine() // '\n' as delim and it not include in buf
size := len(buf)
// buf, err := brw.ReadBytes('B') // 'B' included in buf or error return
// size := len(buf)
msg := fmt.Sprintf("4recv %s [%d]: %x", c.who, size, string(buf))
// c.TextFrame(msg)
log.Println(msg, " ok:", ok)
if err != nil && err == io.EOF { // client disconnect
c.netconn.Close()
break
} else if err != nil {
log.Printf("Error: %#+v", err)
}
}
}
func (c wsdemo) read_unworking() {
input := bufio.NewScanner(c.netconn)
buf := []byte{}
input.Buffer(buf, 2)
c.netconn.SetDeadline(time.Now().Add(100 * time.Millisecond))
log.Printf("read by bufio.Scanner...")
for input.Scan() {
c.netconn.SetDeadline(time.Now().Add(100 * time.Millisecond))
msg := fmt.Sprintf("4recv %s: 0x%x", c.who, input.Text())
c.TextFrame(msg)
log.Println(msg)
}
}
func (c wsdemo) onSocketioMessage(data []byte, fm FrameParser) {
msgtype := SocketioPacket(data[0:1])
switch msgtype {
case SIO_Binary_Event:
packet := []string{}
json.Unmarshal(data[3:], &packet)
event := packet[0]
c.SocketioEventDemo(event, packet[1])
log.Printf("onSocketio BinaryEvent %s %s raw[%d]", event, packet[1], len(data))
case SIO_Event:
packet := []string{}
json.Unmarshal(data[1:], &packet)
event := packet[0]
c.SocketioEventDemo(event, packet[1])
log.Printf("onSocketio Event %s %s raw[%d]", event, packet[1], len(data))
default:
log.Printf("onSocketioPacet %s %s raw[%d]: %x", msgtype.String(), string(data), len(data), data)
}
}
func (c wsdemo) onEngineioPacket(data []byte, fm FrameParser) {
msgtype := EngineioPacket(data[0:1])
log.Printf("onEnginioMessage %s %s raw[%d]: % x", msgtype.String(), string(data), len(data), data)
if msgtype == EIO_Ping {
c.EngineioPond("Pong...")
} else if msgtype == EIO_Message {
c.onSocketioMessage(data[1:], fm)
}
}
func (c wsdemo) onMessage(fm FrameParser) {
// msg := fmt.Sprintf("%s from %s Len:%d 0x%X", fm.Type.String(), c.who, fm.Length, fm.Data)
// c.TextFrame(msg)
// log.Println("onMessage and reply:", msg)
if fm.Type == CloseMessage {
log.Println("Close connection for " + fm.Type.String() + string(fm.Data))
c.netconn.Close()
} else if fm.Type == TextMessage {
c.onEngineioPacket(fm.Data, fm)
} else {
log.Printf("onMessage %s data:%s raw[%d]: % x", fm.Type.String(), string(fm.Data), len(fm.Raw), fm.Raw)
}
}
func (c wsdemo) computeAcceptKey(challengeKey string) string {
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
sn := append([]byte(challengeKey), keyGUID...)
h := sha1.New()
h.Write(sn)
// hash := sha1.Sum(sn)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func (c wsdemo) TextFrame(payload string) {
if len(payload) > 125 {
payload = payload[0:124-len(TOO_LONG)] + TOO_LONG
}
len := byte(len(payload))
frame := []byte{0x80 | byte(TextMessage), len}
frame = append(frame, []byte(payload)...)
c.send(frame)
}
func (c wsdemo) BinaryFrame(payload string) {
if len(payload) > 125 {
payload = payload[0:124-len(TOO_LONG)] + TOO_LONG
}
len := byte(len(payload))
frame := []byte{0x80 | byte(BinaryMessage), len}
frame = append(frame, []byte(payload)...)
c.send(frame)
}
func (c wsdemo) CloseFrame(payload string) {
if len(payload) > 125 {
payload = payload[0:124-len(TOO_LONG)] + TOO_LONG
}
len := byte(len(payload))
frame := []byte{0x80 | byte(CloseMessage), len}
frame = append(frame, []byte(payload)...)
c.send(frame)
}
func (c wsdemo) PingFrame(payload string) {
if len(payload) > 125 {
payload = payload[0:124-len(TOO_LONG)] + TOO_LONG
}
len := byte(len(payload))
frame := []byte{0x80 | byte(PingMessage), len}
frame = append(frame, []byte(payload)...)
c.send(frame)
}
func (c wsdemo) PongFrame(payload string) {
if len(payload) > 125 {
payload = payload[0:124-len(TOO_LONG)] + TOO_LONG
}
len := byte(len(payload))
frame := []byte{0x80 | byte(PongMessage), len}
frame = append(frame, []byte(payload)...)
c.send(frame)
}
func (c wsdemo) send(frame []byte) {
c.netconn.SetWriteDeadline(time.Now().Add(30 * time.Second))
c.brw.Write(frame)
c.brw.Writer.Flush()
// c.brw.Flush()
}
func (c wsdemo) OpenHandshake() {
challengeKey := c.r.Header.Get("Sec-WebSocket-Key")
Extensions := c.r.Header.Get("Sec-WebSocket-Extensions")
Protocol := c.r.Header.Get("Sec-WebSocket-Protocol")
p := []byte{}
p = append(p, "HTTP/1.1 101 Switching Protocols\r\n"...)
p = append(p, "Upgrade: websocket\r\n"...)
p = append(p, "Connection: Upgrade\r\n"...)
if Protocol > "" {
p = append(p, ("Sec-WebSocket-Protocol: " + Protocol + "\r\n")...)
}
if false && Extensions > "" {
p = append(p, ("Sec-WebSocket-Extensions: permessage-deflate\r\n")...)
}
p = append(p, ("Sec-WebSocket-Accept: " + c.computeAcceptKey(challengeKey) + "\r\n")...)
p = append(p, "\r\n"...)
// Clear deadlines set by HTTP server.
c.netconn.SetDeadline(time.Time{})
log.Printf("OpenHandshake %s...", c.who)
c.netconn.Write(p)
}
func (c wsdemo) Upgrade(w http.ResponseWriter, r *http.Request) (net.Conn, *bufio.ReadWriter, error) {
hj, ok := w.(http.Hijacker)
if !ok {
msg := "websocket: response does not implement http.Hijacker"
http.Error(w, msg, http.StatusInternalServerError)
return nil, nil, errors.New(msg)
}
var brw *bufio.ReadWriter
netConn, brw, err := hj.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, nil, err
}
if brw.Reader.Buffered() > 0 {
netConn.Close()
msg := "websocket: client sent data before handshake is complete"
http.Error(w, msg, http.StatusInternalServerError)
return nil, nil, errors.New(msg)
}
return netConn, brw, nil
}
/*
Engine.io API
packet format: <packet type id>[<data>]
for example, a ping message with text: 2probe
single-packet format: <packet1>
multi-packet format: <length1>:<packet1>[<length2>:<packet2>[...]]
message type:
- 0 `open` Sent from the server when a new transport is opened (recheck)
- 1 `close` Request the close of this transport but does not shutdown the connection itself.
- 2 `ping` Sent by the client. Server should answer with a pong packet containing the same data
- 3 `pong` Sent by the server to respond to ping packets.
- 4 `message` actual message, client and server should call their callbacks with the data.
- 5 `upgrade` Before engine.io switches a transport, it tests, if server and client can communicate over this transport.
If this test succeed, the client sends an upgrade packets which requests the server to flush its cache
on the old transport and switch to the new transport.
- 6 `noop` A noop packet. Used primarily to force a poll cycle when an incoming websocket connection is received.
*/
func (c wsdemo) EngineioOpen(text string) {
// Engine.io Open Message
packet := []byte(fmt.Sprintf(`0{"sid":"client_%s","upgrades":[],"pingInterval":15000,"pingTimeout":5000}`, c.who))
c.TextFrame(string(packet))
c.TextFrame("40")
}
func (c wsdemo) EngineioClose(text string) {
c.TextFrame("1" + text)
}
func (c wsdemo) EngineioPing(text string) {
c.TextFrame("2" + text)
}
func (c wsdemo) EngineioPond(text string) {
c.TextFrame("3" + text)
}
func (c wsdemo) EngineioMessage(text string) {
c.TextFrame("4" + text)
}
func (c wsdemo) EngineioUpgrade(text string) {
c.TextFrame("5" + text) // ???
}
func (c wsdemo) EngineioNoop(text string) {
c.TextFrame("6" + text)
}
/*
Socket.io API
- Packet#CONNECT (0)
- Packet#DISCONNECT (1)
- Packet#EVENT (2)
- Packet#ACK (3)
- Packet#ERROR (4)
- Packet#BINARY_EVENT (5)
50-["protobuf", "CgxoZWxsbyBiaW5hcnkQYxoDQUJDIyoIZ29vZCBieWUk"]
- Packet#BINARY_ACK (6)
*/
func (c wsdemo) SocketioEventDemo(event string, data string) {
res := fmt.Sprintf("%s received[%d]", event, len(data))
jsontxt := fmt.Sprintf("{%q:%q,%q:%q}", "userId", c.who, "text", res)
event = "res"
c.SocketioEventEmit(event, jsontxt)
}
func (c wsdemo) SocketioEventEmit(event string, text string) {
// enc := base64.StdEncoding.EncodeToString([]byte(text))
msg := fmt.Sprintf("2[%q,%s]", event, text)
c.EngineioMessage(msg)
}
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {
var output = document.getElementById("output");
var input = document.getElementById("input");
var ws;
var print = function(message) {
var d = document.createElement("div");
d.innerHTML = message;
output.appendChild(d);
};
document.getElementById("open").onclick = function(evt) {
if (ws) {
return false;
}
ws = new WebSocket("{{.}}");
ws.onopen = function(evt) {
print("OPEN");
}
ws.onclose = function(evt) {
print("CLOSE");
ws = null;
}
ws.onmessage = function(evt) {
print("?? " + evt.data);
}
ws.onerror = function(evt) {
print("ERROR: " + evt.data);
}
return false;
};
document.getElementById("send").onclick = function(evt) {
if (!ws) {
return false;
}
print("? " + input.value);
ws.send(input.value);
return false;
};
document.getElementById("close").onclick = function(evt) {
if (!ws) {
return false;
}
ws.close();
return false;
};
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>1. <b>Open</b> a websocket connection to the server.
<p>2. <b>Send</b> a message to the server.
<p>3. <b>Change</b> the message below.
<p>4. <b>Close</b> the connection.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><textarea name="input" id="input" cols="30" rows="10">Hello world!</textarea>
<finput id="finput" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))