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框架的缺陷也很明显,例如:

  1. 为了实现实时聊天,必须轮询每秒提供新消息
  2. 每个客户端每分钟大约60个REST API调用
  3. 服务器将开始被每分钟处理数百万个 REST API 调用所淹没

二、 WebSocket

2.1 WebSocket 与 HTTP 的关系

  1. WebSocket 是一种协议,是一种与 HTTP 同等的网络协议,两者都是应用层协议,都基于 TCP 协议。
  2. WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据
  3. 同时,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))
}

猜你喜欢

转载自blog.csdn.net/weixin_54792212/article/details/129012221
今日推荐