HTTP、WebSocket和Socket.IO
一、HTTP协议
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)。HTTP 协议和 TCP/IP 协议族内的其他众多的协议相同, 用于客户端和服务器之间的通信。请求访问文本或图像等资源的一端称为客户端, 而提供资源响应的一端称为服务器端。
1.1 短轮询
这种方式下,client 每隔一段时间都会向 server 发送 http 请求,服务器收到请求后,将最新的数据发回给 client。
如图所示,在 client 向 server 发送一个请求活动结束后,server 中的数据发生了改变,所以 client 向 server 发送的第二次请求中,server 会将最新的数据返回给 client。
但这种方式也存在弊端。比如在某个时间段 server 没有更新数据,但 client 仍然每隔一段时间发送请求来询问,所以这段时间内的询问都是无效的,这样浪费了网络带宽。将发送请求的间隔时间加大会缓解这种浪费,但如果 server 更新数据很快时,这样又不能满足数据的实时性。
1.2 Comet
鉴于(短)轮询的弊端,一种基于 HTTP 长连接的 “服务器推” 的技术被 hack 了出来,这种技术被命名为 Comet。
Comet : client 与 server 端保持一个长连接,只有数据发生改变时,server 才主动将数据推送给 client。Comet 又可以被细分为两种实现方式,一种是长轮询机制,一种是流技术
1.2.1 长轮询
client 向 server 发出请求,server 接收到请求后,server 并不一定立即发送回应给 client,而是看数据是否更新,如果数据已经更新了的话,那就立即将数据返回给 client;但如果数据没有更新,那就把这个请求保持住,等待有新的数据到来时,才将数据返回给 client。
当然了,如果 server 的数据长时间没有更新,一段时间后,请求便会超时,client 收到超时信息后,再立即发送一个新的请求给 server。
如图所示,在长轮询机制下,client 向 server 发送了请求后,server会等数据更新完才会将数据返回,而不是像(短)轮询一样不管数据有没有更新然后立即返回。
这种方式也有弊端。当 server 向 client 发送数据后,必须等待下一次请求才能将新的数据发送出去,这样 client 接收到新数据的间隔最短时间便是 2 * RTT(往返时间),这样便无法应对 server 端数据更新频率较快的情况。
1.2.2 流技术
流技术基于 Iframe。Iframe 是 HTML 标记,这个标记的 src 属性会保持对指定 server 的长连接请求,server 就可以不断地向 client 返回数据。
可以看出,流技术与长轮询的区别是长轮询本质上还是一种轮询方式,只不过连接的时间有所增加,想要向 server 获取新的数据,client 只能一遍遍的发送请求;而流技术是一直保持连接,不需要 client 请求,当数据发生改变时,server 自动的将数据发送给 client。
如图所示,client 与 server 建立连接之后,便不会断开。当数据发生变化,server 便将数据发送给 client。
但这种方式有一个明显的不足之处,网页会一直显示未加载完成的状态。
上篇文章也介绍了REST API方法,但是RESTFUL框架的缺陷也很明显,例如:
- 为了实现实时聊天,必须轮询每秒提供新消息
- 每个客户端每分钟大约60个REST API调用
- 服务器将开始被每分钟处理数百万个 REST API 调用所淹没
二、 WebSocket
2.1 WebSocket 与 HTTP 的关系
- WebSocket 是一种协议,是一种与 HTTP 同等的网络协议,两者都是应用层协议,都基于 TCP 协议。
- WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据
- 同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了
2.2 WebSocket 原理
相比于传统 HTTP 的每次“请求-应答”都要 client 与 server 建立连接的模式,WebSocket 是一种长连接的模式。
就是一旦 WebSocket 连接建立后,除非 client 或者 server 中有一端主动断开连接,否则每次数据传输之前都不需要 HTTP 那样请求数据。
首先,client 发起 WebSocket 连接,报文类似于 HTTP,但主要有几点不一样的地方:
- “Upgrade: websocket”: 表明这是一个 WebSocket 类型请求,意在告诉 server 需要将通信协议切换到 WebSocket
- “Sec-WebSocket-Key: *”: 是 client 发送的一个 base64 编码的密文,要求 server 必须返回一个对应加密的 “Sec-WebSocket-Accept” 应答,否则 client 会抛出 “Error during WebSocket handshake” 错误,并关闭连接
server 收到报文后,如果支持 WebSocket 协议,那么就会将自己的通信协议切换到 WebSocket,返回以下信息:
- “HTTP/1.1 101 WebSocket Protocol Handshake”:返回的状态码为 101,表示同意 client 的协议转换请求
- “Upgrade: websocket”
- “Connection: Upgrade”
- “Sec-WebSocket-Accept: *”
- …
以上都是利用 HTTP 协议完成的。这样,经过“请求-相应”的过程, server 与 client 的 WebSocket 连接握手成功,后续便可以进行 TCP 通讯了,也就没有 HTTP 什么事了。
三、Socket.IO
3.1 Socket.IO的介绍
Socket.IO 是一个封装了 Websocket、基于 Node 的 JavaScript 框架,包含 client 的 JavaScript 和 server 的 Node。其屏蔽了所有底层细节,让顶层调用非常简单。
另外,Socket.IO 还有一个非常重要的好处。其不仅支持 WebSocket,还支持许多种轮询机制以及其他实时通信方式,并封装了通用的接口。这些方式包含 Adobe Flash Socket、Ajax 长轮询、Ajax multipart streaming 、持久 Iframe、JSONP 轮询等。换句话说,当 Socket.IO 检测到当前环境不支持 WebSocket 时,能够自动地选择最佳的方式来实现网络的实时通信。
3.2 基于go使用Socket.IO
3.2.1 下载和安装包
go get "github.com/googollee/go-socket.io"
import socketio "github.com/googollee/go-socket.io"
3.2.2 gin-gonic
main.go
package mainimport ("fmt""log""net/http""github.com/gin-gonic/gin"socketio "github.com/googollee/go-socket.io"
)func main() {router := gin.New()server := socketio.NewServer(nil)//OnConnect set a handler function f to handle open event for namespace.//连接成功server.OnConnect("/", func(sock socketio.Conn) error {sock.SetContext("")sock.Join("chat")fmt.Println("connect success!", sock.ID())return nil})//接收"bye"事件//OnEvent设置处理程序函数f以处理命名空间的事件server.OnEvent("/", "bye", func(sock socketio.Conn) string {last := sock.Context().(string)sock.Emit("bye", last)sock.Close()fmt.Println("============>", last)return last})server.OnEvent("/chat", "msg", func(sock socketio.Conn, msg string) string {sock.SetContext(msg)fmt.Println("====chat===>", msg)return "server发送的:" + msg})server.OnEvent("/", "notice", func(sock socketio.Conn, msg string) {log.Println("notice:", msg)sock.Emit("reply", "have "+msg)})server.OnError("/", func(sock socketio.Conn, err error) {log.Println("meet error:", err)})server.OnDisconnect("/", func(sock socketio.Conn, reason string) {log.Println("close connect: ", reason)})go func() {if err := server.Serve(); err != nil {log.Fatalf("socketio listen error: %s\n", err)}}()defer server.Close()router.GET("/socket.io/", gin.WrapH(server))router.POST("/socket.io/", gin.WrapH(server))router.StaticFS("/public", http.Dir("./asset"))log.Println("Server at localhost:8000...")if err := router.Run(":8080"); err != nil {log.Fatal("run failed ", err)}
./asset/index.html
<!doctype html>
<html><head><title>Socket.IO chat</title><style>* { margin: 0; padding: 0; box-sizing: border-box; }body { font: 13px Helvetica, Arial; }form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }#messages { list-style-type: none; margin: 0; padding: 0; }#messages li { padding: 5px 10px; }#messages li:nth-child(odd) { background: #eee; }</style></head><body><ul id="messages"></ul><form action=""><input id="m" autocomplete="off" /><button>Send</button></form><script src="https://cdn.socket.io/socket.io-1.2.0.js"></script><script src="https://code.jquery.com/jquery-1.11.1.js"></script><script>var socket = io();var s2 = io("/chat");socket.on('reply', function(msg){$('#messages').append($('<li>').text(msg));});$('form').submit(function(){s2.emit('msg', $('#m').val(), function(data){$('#messages').append($('<li>').text('ACK CALLBACK: ' + data));});socket.emit('notice', $('#m').val());$('#m').val('');return false;});</script></body>
</html>
测试用例:
3.2.3 default-http
main.go
package mainimport ("log""net/http"socketio "github.com/googollee/go-socket.io""github.com/googollee/go-socket.io/engineio""github.com/googollee/go-socket.io/engineio/transport""github.com/googollee/go-socket.io/engineio/transport/polling""github.com/googollee/go-socket.io/engineio/transport/websocket"
)// Easier to get running with CORS. Thanks for help @Vindexus and @erkie
var allowOriginFunc = func(r *http.Request) bool {return true
}func main() {server := socketio.NewServer(&engineio.Options{Transports: []transport.Transport{&polling.Transport{CheckOrigin: allowOriginFunc,},&websocket.Transport{CheckOrigin: allowOriginFunc,},},})server.OnConnect("/", func(s socketio.Conn) error {s.SetContext("")log.Println("connected:", s.ID())return nil})server.OnEvent("/", "notice", func(s socketio.Conn, msg string) {log.Println("notice:", msg)s.Emit("reply", "have "+msg)})server.OnEvent("/chat", "msg", func(s socketio.Conn, msg string) string {s.SetContext(msg)return "recv " + msg})server.OnEvent("/", "bye", func(s socketio.Conn) string {last := s.Context().(string)s.Emit("bye", last)s.Close()return last})server.OnError("/", func(s socketio.Conn, e error) {log.Println("meet error:", e)})server.OnDisconnect("/", func(s socketio.Conn, reason string) {log.Println("closed", reason)})go func() {if err := server.Serve(); err != nil {log.Fatalf("socketio listen error: %s\n", err)}}()defer server.Close()http.Handle("/socket.io/", server)http.Handle("/", http.FileServer(http.Dir("./asset")))log.Println("Serving at localhost:8000...")log.Fatal(http.ListenAndServe(":8000", nil))
}