WebSocket
- WebSocket 是独立的、创建在 TCP 上的协议。
- WebSocket在 HTML5 游戏和网页消息推送都使用比较多。WebSocket 是 HTML5 的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。
- 它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。
与HTTP长连接的区别
类似Socket的TCP长连接通讯:首先WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求
相比HTTP长连接WebSocket的特点:
- 是真正的全双工方式,建立连接后客户端与服务器端都可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。
- Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。
- 不同的URL可以复用同一个WebSocket连接等功能。这是HTTP长连接不能做到的。
- 连接建立后定期的心跳检测
- 浏览器请求头:
General: Request URL: ws://127.0.0.1:8080/ws Request Method: GET Status Code: 101 Switching Protocols Response Headers: //服务端返回的header Connection: Upgrade Sec-WebSocket-Accept: 37En2yJBKtzkzTbOExAi+tc6DRs= Upgrade: websocket //客户端请求的header Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6 Cache-Control: no-cache Connection: Upgrade Host: 127.0.0.1:8080 Origin: file:// Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: cRYRFb0jzm06V3NPbnRjmw== Sec-WebSocket-Version: 13 Upgrade: websocket User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
1.Upgrade: websocket :表明这是WebSocket类型请求;
2. Sec-WebSocket-Key: cRYRFb0jzm06V3NPbnRjmw== :是WebSocket客户端发送的一个 base64编码的密文(浏览器随机生成的),要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答(对key拼接一个GUID后,sha-1加密后,再经过base64编码),否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。
3.经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了
Go中websocket的服务端代码实现
- 使用github.com/gorilla/websocket包
- websocket重点在于协议转换的过程:通过Upgrade函数实现
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {}
该函数的功能主要是:
判断请求方法是否为GET,不是GET则为非法握手方法
根据client的请求头信息,确认升级协议
校验跨域
填充响应头,并返回客户端,链接建立
type WsServer struct {
listener net.Listener
addr string
upgrade *websocket.Upgrader
}
func NewWsServer() *WsServer {
ws := new(WsServer)
ws.addr = "0.0.0.0:8080"
ws.upgrade = &websocket.Upgrader{
// 指定升级 websocket 握手完成的超时时间
HandshakeTimeout :time.Second*5,
// 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
//WriteBufferPool BufferPool
//按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
//Subprotocols []string
// 指定 http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
//Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
// EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。
// 将此值设置为true并不能保证将支持压缩。
// 目前仅支持“无上下文接管”模式
//EnableCompression bool
// 指定 io 操作的缓存大小,如果不指定就会自动分配。
ReadBufferSize:1024,
WriteBufferSize:1024,
// 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数。
// 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:
// 如果Origin请求头存在且原始主机不等于请求主机头,则返回false
CheckOrigin: func(r *http.Request) bool {
if r.Method != "GET" {
fmt.Println("method is not GET")
return false
}
if r.URL.Path != "/ws" {
fmt.Println("path error")
return false
}
return true
},
}
return ws
}
//往conn中发送信息
func (w *WsServer) send(conn *websocket.Conn, stopCh chan int) {
//w.send10(conn)
for {
select {
case <-stopCh:
fmt.Println("connect closed")
return
case <-time.After(time.Second * 1):
data := fmt.Sprintf("hello websocket(timeStamp: %v)", time.Now().UnixNano())
err := conn.WriteMessage(1, []byte(data))
fmt.Println("sending....")
if err != nil {
fmt.Println("send msg faild ", err)
return
}
}
}
}
//从conn中读取信息
func (w *WsServer) connHandle(conn *websocket.Conn) {
stopCh := make(chan int)
defer func() {
close(stopCh)
conn.Close()
}()
go w.send(conn, stopCh)
for {
//conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(5000)))
// ping协程
go func() {
tick := time.NewTicker(5 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C:
conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(time.Second))
case <-stopCh:
return
}
}
}()
_, msg, err := conn.ReadMessage()
if err != nil {
close(stopCh)
// 判断是不是超时
if netErr, ok := err.(net.Error); ok {
if netErr.Timeout() {
fmt.Printf("ReadMessage timeout remote: %v\n", conn.RemoteAddr())
return
}
}
// 其他错误,如果是 1001 和 1000 就不打印日志
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
fmt.Printf("ReadMessage other remote:%v error: %v \n", conn.RemoteAddr(), err)
}
return
}
fmt.Println("收到消息:", string(msg))
}
}
//WsServer需要实现了Handler这个接口类
func (w *WsServer) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/ws" {
httpCode := http.StatusInternalServerError
statusCodeMsg := http.StatusText(httpCode)
fmt.Println("500:", statusCodeMsg)
http.Error(rsp, statusCodeMsg, httpCode)
return
}
conn, err := w.upgrade.Upgrade(rsp, req, nil)
if err != nil {
fmt.Println("websocket error:", err)
return
}
fmt.Println("client connect :", conn.RemoteAddr())
go w.connHandle(conn)
}
服务开启:
func main() {
w:= NewWsServer()
w.listener, err = net.Listen("tcp", w.addr)
if err != nil {
fmt.Println("net listen error:", err)
return
}
//WsServer实现了Handler这个接口类所以第二个参数可以直接传w
err = http.Serve(w.listener, w)
if err != nil {
fmt.Println("http serve error:", err)
return
}
}
客户端
通过golang.org/x/net/websocket包来dial发送消息
type Client struct {
Host string
Path string
}
func NewWebsocketClient(host, path string) *Client {
return &Client{
Host: host,
Path: path,
}
}
func (c *Client) SendMessage(body []byte) error {
u := url.URL{Scheme: "ws", Host: c.Host, Path: c.Path}
//使用golang.org/x/net/websocket来dial发送消息
ws, err := websocket.Dial(u.String(), "", "http://"+c.Host+"/")
defer ws.Close() //关闭连接
if err != nil {
fmt.Println("websocket dial err:",err)
return err
}
_, err = ws.Write(body)
if err != nil {
fmt.Println("websocket Write err:",err)
return err
}
return nil
}
func main(){
wc:=NewWebsocketClient("127.0.0.1:8080","ws")
wc.SendMessage([]byte("hello"))
}
前端Js请求:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Sample of websocket with golang</title>
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
$(function() {
var $ul = $('#msg');
var ws = new WebSocket('ws://127.0.0.1:8080/ws');
ws.onopen = function(e) {
$('<li>').text("connected").appendTo($ul);
}
ws.onmessage = function(e) {
$('<li>').text(e.data).appendTo($ul);
};
});
</script>
</head>
<body>
<ul id="msg"></ul>
</body>
</html>
浏览器访问结果:
connected
hello websocket(timeStamp: 1596425101756145600)
hello websocket(timeStamp: 1596425102756530000)
hello websocket(timeStamp: 1596425103771658100)
hello websocket(timeStamp: 1596425104771919200)
hello websocket(timeStamp: 1596425105771982700)
hello websocket(timeStamp: 1596425106771997900)
hello websocket(timeStamp: 1596425107782578100)
hello websocket(timeStamp: 1596425108794537100)
hello websocket(timeStamp: 1596425109799885400)
hello websocket(timeStamp: 1596425110800988500)
...