造轮子 Websocket 现在就 Go
MD: 2019年12月17日,03:45:10
笔者坚果有幸从事软件开发,一直都是兴趣驱动的工作。第一次接触计算机是 1999 年后的事,我用来学习的电脑是大哥买来准备学 CAD 的 486 机,当时 CPU 还是威胜的 333MHz 主频,硬盘也只有 4GB,系统是 Windows 98 SE。那时所谓的学电脑纯属拆玩具模式,因为手上可用的资源不多,网络也不发达,也没有太丰富的参考资料,相关的图书也不是太丰富。所以翻查硬盘或系统光盘文件成了日常活动,除此之外,DOS 游戏也和红白机具有一样的可玩性。彼时,BAT 脚本和 Windows 系统光盘中 QBasic 脚本编程工具成了不错的好玩具。后来玩起了 Visual Studio 6.0,主要是 VB 和 VBA 脚本编程,C 语言也开始了解一些,C++ 几乎没有基础可言,所以 Visual C++ 一直玩不动 MFC 什么的更是不知云云。当然了,集成开发工具提供的最大的好处也就体现在这,即使你是个傻瓜也能毫不費力地运行配置好的模板程序,编译成完整的可运行程序。不知不觉,坚果从曾经的傻瓜程序员一路走到今天,没有兴趣带领还真不会有今天。
文章目录
内容提要
这是我二进 GitChat 的创造,距离今年 6 月分第一次发布《从 JavaScript 入门到 Vue 组件实践》大谈前端技术全局观、30’ JavaScript 入门课,还有 VSCode 和 Sublime 这些好用的开发工具。到如今已经有近半年时间,期间经历了较大的工作变动,技术上已经以脚本后端转到 Golang 为主,这是一种我一直期待的语言。期间也学到一些技术领域比较不容易学习到的知识,有项目管理层面的,有职业规划方面的,对知识付费时代也有了更深入的理解。
那么 Golang 作为一款以便利的并发编程的语言,用在后端的开发真的是不要太好。
Golang 虽然它已经有 10 岁大了,最早接触也是 2012 年左右,但是真正花心思学起来是今年的 7 月份。Golang 号称 21 世纪的 C 语言,这确实是对我最大的吸引力,它的特点可以总结为 C + OOP,以松散组合的方式去实现面向对象的编程思维。完全不像 C++ 把对象数据模型设计的异常复杂,把一种编程语言搞得自己发明人都不能完全掌握。当然每种语言都有它的适用领域及特点,免不了一堆人贬低 Golang 没有泛型之类,确实 Golang 1.x 就是没有提供实现。如日中天的 Python 就是个典型,作为奇慢的脚本解析型语言,慢这个缺点完全掩盖不了它中人工智能算法领域的应用,也完全阻挡不了爬虫一族赖以为生。这种取舍其实就是一种效益的体现,选择恰当的工具做适合的事!
我们将从网络协议层面来打开 Golang 编程大门,学习关于 Websocket 网络协议的相关知识点,在 TCP/IP 协议栈中,新加入的 Websocket 分量也是重量级的。WebSocket 作为实时性要求较高场合的应用协议,主要应用在在线网页聊天室、页游行业等等。掌握 Websocket 技能,你值得拥有!
在本轮学习中,你可以 Get 到技能:
- 如何拥有快速掌握一种计算机语言的能力;
- 理解几个基本网络概念:
- Persistent connection 长连接;
- Temporary connection 短连接;
- Polling 轮询;
- LongPolling 长轮询;
- Websocket 核心的数据帧 Data Framing 构造;
- Websocket 握手连接建立数据通讯过程;
- 实现一个 go-my-websocket 简约版 Websocket 服务器;
- 深入分解 Golang 的 Engine.io 及 Socket.io 的应用;
- 获得一份完整的电子版 PDF;
- 获得一份完整的 go-my-websocket 代码;
- 通过交流活动获得问题解答机会;
从任天堂红白机时代接触单片机,尽管那时不懂却被深深吸引了;从 MS-DOS 时代结缘计算机,就这样一路披荆斩棘前行;很多人说 IT 人是吃青春饭的,对于我,一个 80 后,在乳臭未干的时候就闻到这饭香了,到现在也没觉得吃够吃厌倦了。我只当这是个兴趣,现在这个爱好还能给我带来一份收入而已。
by Jeangowhy 微信同名(jimboyeah◉gmail.com)
Tue Dec 17 2019 04:23:08 o/
如果不想使用 GitChat 可以参考未整理版本参考 https://www.jianshu.com/p/c44f556de0ddjian
配套代码 https://github.com/jimboyeah/demo
websocket
The WebSocket Protocol https://tools.ietf.org/html/rfc6455
WebSocket vs Polling
先理解几个概念
- Persistent connection 长连接
- Temporary connection 短连接
- Polling 轮询
- LongPolling 长轮询
建立 TCP 连接后,在数据传输完成时还保持 TCP 连接不断开,不发RST包、不进行四次握手断开,并等待对方继续用这个 TCP 通道传输数据,相反的就是短连接。通常 HTTP 连接就是短连接,浏览器建立 TCP 连接请求页面,服务器发送数据,然后关闭连接。下次再需要请求数据时又重新建立 TCP 连接,一问一答是短连接的一个特点。而新的 HTTP 2.0 规范中,为了提高性能则使用了长连接来复用同一个 TCP 连接来传送不同的文件数据。
参考 RFC 2616 HTTP 1.1 规范文档关于 Persistent connection 的部分 https://tools.ietf.org/html/rfc2616#page-44
HTTP 头信息 Connection: Keep-alive 是 HTTP 1.0 浏览器和服务器的实验性扩展,当前的 HTTP 1.1 RFC 2616 文档没有对它做说明,因为它所需要的功能已经默认开启,无须带着它,但是实践中可以发现,浏览器的报文请求都会带上它。如果不希望使用长连接,则要在请求报文首部加上 Connection: close。
《HTTP权威指南》提到,有部分古老的 HTTP 1.0 代理不理解 Keep-alive,而导致长连接失效:客户端–>代理–>服务端,客户端带有 Keep-alive,而代理不认识,于是将报文原封不动转给了服务端,服务端响应了 Keep-alive,也被代理转发给了客户端,于是保持了 客户端-->代理
连接和 代理-->服务端
连接不关闭,但是,当客户端第发送第二次请求时,代理会认为当前连接不会有请求了,于是忽略了它,长连接失效。书上也介绍了解决方案:遇到 HTTP 1.0 就忽略 Keep-alive,客户端就知道当前不该使用长连接。
使用了HTTP长连接(HTTP persistent connection )之后的好处,包括可以使用HTTP 流水线技术(HTTP pipelining,也有翻译为管道化连接),它是指,在一个TCP连接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起。
Client 和 Server 间的实时数据传输是一个很重要的需求,但早期 HTTP 只能通过 AJAX 轮询 Pooling 方式实现,客户端定时向服务器发送 Ajax 请求,服务器接到请求后马上返回响应信息并关闭连接,这就时短连接的应用。轮询带来以下问题:
- 服务器必须为同一个客户端的轮询请求建立不同的 TCP 连接,算上 TCP 的三握手过程,每个 HTTP 连接的建立就需要来回通讯将近 10 次;
- 客户端脚本需要维护出站/入站连接的映射,即管理本地请求与服务器响应的对应关系;
- 请求中有大半是无用,每一次的 HTTP 请求和应答都带有完整的 HTTP 头信息,浪费带宽和服务器资源。
长轮询 LongPolling 是基于长连接实现的,是对 Polling 的一种改进。Client 发送请求,此时 Server 可以发送数据或等待数据准备好:
- 如果 Server 有新的数据需要传送,就立即把数据发回给 Client,收到数据后又立即再发送 HTTP 请求。
- 如果 Server 没有新数据需要传送,与 Polling 的方式不同的是,Server 不是立即发送回应给 Client,而是将这个请求保持住,等待有新的数据来到,再去响应这个请求。
- 如果 Server 长時没有数据响应,这个 HTTP 请求就会超时,Client 收到超时信息后,重新向服务器发送一个 HTTP 请求,循环这个过程。
LongPolling 的方式虽然在某种程度上减少了网络带宽和 CPU 利用率等问题,但仍存在缺陷,因为 LongPolling 还是基于一问一答的 HTTP 协议模式。当 Server 的数据更新速度较快,Server 在传送一个数据包给 Client 后必须等待下一个 HTTP 请求到来,才能传递第二个更新的数据包给 Browser,这种场景在 HTTP 上实现的实时聊天几多人游戏是常见的。这样的话,Browser 显示实时数据最快的时间为 2 倍 RTT 往返时间。还不考虑网络拥堵的情况,这个应该是不能让用户接受的。另外,由于 HTTP 数据包的头部数据量很大,通常有 400 多个字节,但真正被服务器需要的数据却很少,可能只有 10个字节左右,这样的数据包在网络上周期性传输,难免对网络带宽是一种浪费。
WebSocket 正是基于支持客户端和服务端的双向通信、简化协议头这些需求下,基于 HTTP 和 TCP 的基础上登上了 Web 的舞台。由于 HTTP 协议的原设计不是用来做双向通讯的,它只是一问一答的执行,客户端发送请求,服务器进行答复,客服端需要什么文件,服务器就提供文件数据。
WebSocket 通信协议是一种双向通信协议,在建立连接后,WebSocket 服务器和 Client 都能主动的向对象发送或接收数据,就像 Socket 一样,所以建立在 Web 基础上的 WebSocket 需要通过升级 HTTP 连接来实现类似 TCP 那样的握手连接,连接成功后才能相互通信。相互沟通的 Header 很小,大概只有 2Bytes。服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 Upgrade: WebSocket
表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
因为 WebSocket 是一种新的通信协议,目前还是草案,没有成为标准,市场上也有成熟的实现 WebSocket 协议开源 Library 可供使用。例如 PyWebSocket、 WebSocket-Node、 LibWebSockets 等。
本文介绍内容大致如下:
- Websocket 握手机制细节
- Websocket 数据帧结构
Websocket 协议通信分为两个部分,先是握手,再是数据传输。 主要是建立连接握手 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 子协议查询
- Sec-WebSocket-Version 协议版本号
Websocket协议中如何确保客户端与服务端接收到握手请求呢? 这里就要说到HTTP的两个头部字段,Sec-Websocket-Key
与 Sec-Websocket-Accept
。
-
首先客户端发起请求,在头部
Sec-Websocket-Key
中随机生成 base64 字符串;Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
-
服务端收到请求后,根据头部
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 数据帧
根据 RFC 6455 定义,websocket 消息统称为 messages,可以由多个帧 frame 构成。有文本数据帧,二进制数据帧,控制帧三种,Websocket 官方定义有 6 种类型并预留了 10 种类型用于未来的扩展。
了解完 websocket 握手的大致过程后,这个部分介绍下 Websocket 数据帧与分片传输的方式,主要头部信息是前面的 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| | |
+-+-+