一、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 main
import (
"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 main
import (
"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))
}