Socket.IO 是什么

Socket.IO 是一个库,可以在客户端和服务器之间实现 低延迟双向 和 基于事件的 通信。

Socket.IO 连接可以通过不同的传输建立:

  • HTTP 长轮询
  • WebSocket

Socket.IO 将自动选择最佳可用选项,具体取决于:

  • 浏览器的功能(请参阅此处此处
  • 网络配置(某些网络阻止 WebSocket 和/或 WebTransport 连接)

如果在应用程序中使用普通的 WebSocket,则需要自己实现 Socket.IO 中已经包含(并经过实战测试)的大部分功能,例如重新连接,回调或广播,也就是下面列出的特性

注意:Socket.IO不是WebSocket的实现,虽然 Socket.IO确实在可能的情况下会去使用WebSocket作为一个transport,但是它添加了很多元数据到每一个报文中:报文的类型以及namespace和ack Id。这也是为什么标准WebSocket客户端不能够成功连接上 Socket.IO 服务器,同样一个 Socket.IO 客户端也连接不上标准WebSocket服务器的原因

运作原理

Socket.IO 服务器 (Node.js) 和 Socket.IO 客户端(浏览器, Node.js 或 其他编程语言)之间的双向通道尽可能使用WebSocket 连接建立,并将使用 HTTP 长轮询作为后备。

Socket.IO 代码库分为两个不同的层:

  • 底层通道:我们称之为Engine.IO,Socket.IO内部的引擎
  • 高级 API:Socket.IO 本身

Engine.IO 负责建立服务器和客户端之间的低级连接。它处理:

特点

以下是 Socket.IO 在普通 WebSockets 上提供的功能:

HTTP 长轮询回退

如果无法建立 WebSocket 连接,连接将回退到 HTTP 长轮询。

这个特性是人们在十多年前创建项目时使用 Socket.IO 的原因,因为浏览器对 WebSockets 的支持仍处于起步阶段。

即使现在大多数浏览器都支持 WebSockets(超过97%),它仍然是一个很棒的功能,因为我们仍然会收到来自用户的报告,这些用户无法建立 WebSocket 连接,因为他们使用了一些错误配置的代理。

短线检测

Engine.IO 连接在以下情况下被视为关闭:

  • 一个 HTTP 请求(GET 或 POST)失败(例如,当服务器关闭时)
  • WebSocket 连接关闭(例如,当用户关闭其浏览器中的选项卡时)
  • socket.disconnect() 在服务器端或客户端调用

还有一个心跳机制检查服务器和客户端之间的连接是否仍然正常运行:

在给定的时间间隔( pingInterval握手中发送的值),服务器发送一个 PING 数据包,客户端有几秒钟(该pingTimeout值)发送一个 PONG 数据包。如果服务器没有收到返回的 PONG 数据包,则认为连接已关闭。反之,如果客户端在 pingInterval + pingTimeout 内没有收到 PING 包,则认为连接已关闭。

pingInterval 默认值为 25000
pingTimeout 默认值为 20000

https://socket.io/zh-CN/docs/v4/server-options/#pinginterval

自动重新连接

在某些特定情况下,服务器和客户端之间的 WebSocket 连接可能会中断,而双方都不知道链接的断开状态。

这就是为什么 Socket.IO 包含一个心跳机制,它会定期检查连接的状态。

当客户端最终断开连接时,它会以指数回退延迟自动重新连接,以免使服务器不堪重负。

数据包缓冲

当客户端断开连接时,数据包会自动缓冲,并在重新连接时发送。

收到后的回调

Socket.IO 提供了一种方便的方式来发送事件和接收响应:

发送人

socket.emit("hello", "world", (response) => {
  console.log(response); // "got it"
});

接收者

socket.on("hello", (arg, callback) => {
  console.log(arg); // "world"
  callback("got it!");
});

超时

socket.timeout(5000).emit("hello", "world", (err, response) => {
  if (err) {
    // 另一方未在给定延迟内确认事件
  } else {
    console.log(response); // "got it"
  }
});

广播 Broadcasting

在服务器端,您可以向所有连接的客户端客户端的子集发送事件,称之为“房间”

// 到所有连接的客户端
io.emit("hello");

// 致“news”房间中的所有连接客户端
io.to("news").emit("hello");

这在扩展到多个节点时也有效

多路复用 Multiplexing

socket.io 称之为“命名空间”,命名空间允许您在单个共享连接上拆分应用程序的逻辑。例如,如果您想创建一个只有授权用户才能加入的“管理员”频道,这可能很有用。

io.on("connection", (socket) => {
  // 普通用户
});

io.of("/admin").on("connection", (socket) => {
  // 管理员用户
});

数据格式

socket.emit("hello", "world") 将作为单个 WebSocket 帧发送,其中包含42["hello","world"]

  • 4 是 Engine.IO “消息”数据包类型
  • 2 是 Socket.IO “消息”数据包类型
  • ["hello","world"]是参数数组被JSON.stringify()过的版本

因此,每条消息都会增加几个字节,可以通过使用自定义解析器进一步减少。

以下是可用数据包类型的列表:

TypeIDUsage
open0Used during the handshake. 握手时使用
close1Used to indicate that a transport can be closed. 关闭
ping2Used in the heartbeat mechanism. 心跳机制
pong3Used in the heartbeat mechanism. 心跳机制
message4Used to send a payload to the other side. 发送数据
upgrade5Used during the upgrade process. 升级 websocket
noop6Used during the upgrade process. 升级 websocket

SocketIO的限制

首先是初始连接比WebSockets更长。这是因为它首先使用长轮询和XHRPolling建立连接,然后升级到WebSocket(如果可用)。如果您不需要支持较旧的浏览器并且不担心不支持WebSockets的客户端环境,则可能不需要SocketIO的额外开销。您可以通过指定仅与WebSockets连接来最大程度地减少这种影响。这将更改与WebSocket的初始连接,但是会关闭备选方案。

socket.io必须使用官方的连接方式,跟 websocket 不通用。

示例

golang server 实现

类库 https://github.com/googollee/go-socket.io

server 监听3030端口,

package main

import (
	"fmt"
	"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"
	"github.com/rs/cors"
)

func main() {
	//socketio 服务对象
	pt := polling.Default

	wt := websocket.Default
	wt.CheckOrigin = func(req *http.Request) bool {
		return true
	}
	server := socketio.NewServer(&engineio.Options{
		//PingInterval: pingInterval,
		//PingTimeout:  pingTimeout,
		Transports: []transport.Transport{
			pt,
			wt,
		},
	})
	server.OnConnect("/", func(s socketio.Conn) error {
		s.SetContext("")
		fmt.Println("connected:", s.ID())
		return nil
	})
	server.OnEvent("/", "notice", func(s socketio.Conn, msg string) {
		fmt.Println("notice:", msg)
		s.Emit("reply", "have "+msg)
	})
	server.OnEvent("/", "client_message", func(s socketio.Conn, msg string) {
		fmt.Println("client_message:", 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) {
		// server.Remove(s.ID())
		fmt.Println("报错:meet error:", e)
	})
	server.OnDisconnect("/", func(s socketio.Conn, reason string) {
		// Add the Remove session id. Fixed the connection & mem leak
		fmt.Println("关闭closed", reason)
	})
	go server.Serve()
	defer server.Close()
	// 设置CORS中间件选项
	c := cors.New(cors.Options{
		AllowedOrigins:   []string{"http://lion.oa.com"}, // 明确指定允许的源
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowCredentials: true, // 允许请求携带凭证
		// 可以根据需要添加其他CORS配置项
	})
	log.Println("Serving at localhost:3030...")
	mux := http.NewServeMux()
	mux.Handle("/socket.io/", server)
	handler := c.Handler(mux)
	log.Fatal(http.ListenAndServe(":3030", handler))
}

前端实现,可以起一个 nginx 服务,然后将下面的文件websocket.html发到 www 目录下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Socket.IO Client Example</title>
    <!-- 引入 Socket.IO 客户端库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.0/socket.io.min.js"></script>
</head>
<body>
    <h2>Socket.IO Client Example</h2>
    <input id="emitField" type="text">
    <button onclick="sendMessage()">Send Message</button>
    <div id="messages"></div>
    <script>
        // 初始化 Socket 连接
        const params = {
            transports: ['websocket'], //指定只同意 websocket 方式连接,不轮训,也可以去掉
            query: {
                'socket-id': '0',
            },
        };
        const socket = io('http://localhost:3030/', params); // 替换为服务器地址
        // 发送消息到服务器
        function sendMessage() {
            const message = document.getElementById('emitField').value;
            if(message) {
                socket.emit('notice', message); // 触发服务器监听的事件
                document.getElementById('emitField').value = '';
            }
        }

        socket.on("connect_error", (err) => {
            // the reason of the error, for example "xhr poll error"
            console.log(err.message);

            // some additional description, for example the status code of the initial HTTP response
            console.log(err.description);

            // some additional context, for example the XMLHttpRequest object
            console.log(err.context);
        });
        // 监听连接状态
        socket.on('reply', function(msg) {
            console.log('reply: ' + msg);
        });
        // 监听连接状态
        socket.on('connect', function() {
            console.log('Connected to the server!');
        });

        socket.on('disconnect', function() {
            console.log('Disconnected from the server.');
        });

        // 错误处理
        socket.on('error', function(err) {
            console.error('An error occurred:', err);
        });
    </script>
</body>
</html>

查看client 版本,需要与 server 对应

https://cdnjs.com/libraries/socket.io/1.4.0

启动 server 后,打开前端 html 页面,F12 查看 websocket 连接

相关文章:

https://www.cnblogs.com/zhangmingda/p/12678630.html
http://www.52im.net/thread-3695-1-1.html