上篇文章讲了TCP/IP的一些基础概念,并通过go内置的包实现了socket编程。本篇文章来了解一下另外一个概念——WebSocket。但从命名上来看WebSocket和Socket很类似,但是其实两者并没有直接的联系。Websocket跟HTTP对应,基是于TCP 协议之上的长连接应用层协议。Socket是操作系统抽象出来,方便我们使用TCP/UDP协议编程方法,属于传输层协议。两者其实没有直接联系,就像Java和JavaScript的关系一样。
WebSocket是一种应用层通信协议,可在单个TCP连接上进行全双工通信。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
从上面概念上可以看出WebSocket的一些特点:应用层通信协议、基于单个TCP连接、持久连接、全双工双向数据传输等。这些是比较粗泛的概念,任何一种协议的出现肯定有它的应用场景,下面就简单介绍一下为什么会出现WebSocket这一应用层通信协议。
客户端跟服务器之间的交互一般都是使用HTTP协议进行通信的,相比较Websocket,HTTP最大的问题就是Http是个单工的协议,请求只能由客户端发起,然后服务器收到请求后处理并回传信息,服务器不能主动向客户端发送信息。对于一般的场景,HTTP是比较适用的,因为HTTP是一个单工的短连接协议,可以很好的节省连接维系的成本,客户端服务端完成一次交互后,就会断开TCP连接,以便其它客户端可以获取连接,完成跟服务器的交互。但是一旦出现类似于聊天这种场景,HTTP就不是很适用了,为了实现即时通讯,客户端必须通过“轮询”的方式,在特定的时间间隔内,由客户端对服务器发出 HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新。可以想到,这种方式是有很多缺点的。
为了应付客户端的轮询,HTTP连接必须不断打开关闭,或者一直保持打开的状态,即使服务器的状态没有发生变化,客户端资源也会一直被占用,没有办法被释放
轮询中获得的有效信息不可控,有可能一整天数据才发生一次变化,但是为了获得这个变化,却不得不一整天都在对服务器进行轮询,效率非常低
HTTP请求是包含着各种头部信息的,这就意味着,即使轮询的请求里面没有任何信息,一个HTTP也要有好几百B的数据量,在高并发的状况下进行轮询,流量可能会非常庞大了,而且这些流量都是无用的
虽然比较新的轮询技术,比如Comet。可以实现双向通信,但仍然需要反复发出请求。而且在Comet中普遍采用的HTTP长连接也会消耗服务器资源。基于这种情况,HTML5定义了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框架。
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协议。
接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息。
Hello
WebSocket Echo Test
可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件:
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编程》