WebSocket
WebSocket
概述
在网络分层模型中,应用层协议是我们在开发中最常直接接触到的一层。比如浏览网页使用 HTTP/HTTPS 协议请求数据,开发 Flask 应用前后端交互也是通过 HTTP 协议的接口完成。在极少数对性能要求极高的场景,我们也可能基于传输层的 TCP 或 UDP 自定义协议。很多知名软件都有自己的应用层协议,比如 Kafka 的高效的二进制消息协议,MySQL 在客户端与服务端之间也使用了 独特的通信协议。
应用层协议里我们最常打交道的就是 HTTP 协议,但是 HTTP 本身存在一些限制,在某些实时通信场景下并不方便:
- 单向通信:HTTP 遵循请求-响应模式,客户端必须先请求,服务端才能返回;服务端无法主动推送数据;
- 无状态:每次请求相互独立,默认不会记住之前的上下文(需要 cookie / session / token 等机制来补充);
- 开销较大:即使只传递一条很小的消息,也需要携带完整的 HTTP 头部(几十上百字节),效率不高。
举个例子:假设我们正在开发一个科学计算软件,需要把后端求解器的残差等仿真结果实时传输到前端。如果用 HTTP 实现,只能依靠轮询来“伪实时”获取结果:前端不停地发请求问“有新结果吗?”,不仅延迟大,而且浪费带宽。
在这种场景下,WebSocket 应用层协议就是更好的选择。它具备以下特点:
- 全双工通信:连接建立后,客户端和服务端都可以主动发送消息,实时性强;
- 长连接:只需建立一次连接即可保持不断开,避免了反复握手的开销;
- 轻量消息头:每条消息的帧头只有 2~12 字节,相比 HTTP 的冗余报文轻得多;
- 基于 TCP:同样提供可靠传输的保证。
这使得 WebSocket 特别适合需要实时双向通信的应用场景,比如:聊天室、在线游戏、协同编辑、股票行情推送、科学计算可视化等。
Socket
在传输层有两大常见协议:TCP 和 UDP。
- TCP:面向连接,传输前要通过“三次握手”建立可靠连接。
- UDP:无连接,直接发送数据,不保证可靠性。
但是,仅有协议还不够,应用程序需要一个编程接口来使用它们,这就是 Socket。 Socket 是操作系统提供的网络通信抽象接口,程序员可以通过 Socket API 使用 TCP/UDP 来发送和接收数据。 需要注意,Socket 层面操作的是字节流,并不规定应用层的消息格式。
一个最简单的 TCP Socket 示例
服务端(server.py)
1 | import socket |
在服务端中:
socket(AF_INET, SOCK_STREAM)
表示创建了一个 IPv4 + TCP 的 socket。bind()
将 socket 绑定到本机的地址和端口。listen(backlog)
让 socket 进入监听状态,此时内核会维护两个队列:- 半连接队列:存放只收到了 SYN,还没完成三次握手的连接。
- 完成队列:存放握手已完成、进入
ESTABLISHED
状态,但应用层还没accept()
取走的连接。
backlog
参数就是“完成队列”的长度上限。 这样设计是为了让服务器能够先完成握手,把连接放在队列里,再根据自身处理能力调用 accept()
逐个处理。如果不及时 accept()
,大概会有以下两种情况:
- 情况一:握手完成,但应用层迟迟不
accept()
- 客户端
connect()
会成功返回,并可能立刻发送 HTTP 请求。 - 请求数据进入服务器内核缓冲区,但由于还没有会话 socket,应用层读不到。
- 客户端会一直等待响应,最终超时(例如浏览器报
ERR_CONNECTION_TIMED_OUT
)。
- 客户端
- 情况二:完成队列被塞满
- 新连接要么被丢弃(SYN 不回复),要么被拒绝(RST)。
- 客户端会报
Connection refused
或直接超时。
这里要厘清 listen()
和 accept()
的关系与区别。socket.listen(backlog)
不是阻塞调用,它只是把这个 socket 标记为监听 socket,设置好 backlog 队列的大小,并直接返回控制权给程序。所以调用 listen()
后,程序会立刻继续往下执行,并没有”卡住“来等待连接。那么监听是怎么实现的呢?是操作系统内核在后台自动完成的。一旦调用了 listen()
,这个 socket 就会进入 LISTEN 状态。当客户端发来 SYN 包时,内核会自动回复 SYN+ACK,等待客户端 ACK,完成三次握手,把连接放到已完成队列。
而 accept()
是默认阻塞的,直到有一个握手完成的连接出现在”已完成队列“中。accept()
成功时,就会返回一个新的会话 socket,用于和某个客户端通信,同时返回客户端地址信息。所以总结一下:
listen()
:非阻塞,只是让内核进入监听状态,准备接收连接。accept()
:默认阻塞,取走一个已完成三次握手的连接,生成新的会话 socket。- 三次握手由内核完成,而不是 Python 程序。Python 只是在
accept()
之后,才真正“接管”这个连接的数据交互。
会话 socket 和监听 socket 是不同的。监听 socket (server_socket
):始终只有一个,用来等待新连接;会话 socket (conn
):每个客户端连接对应一个。例如:
1 | while True: |
如果有 3 个客户端接入,那么第一次 accept()
返回 conn1,...,第三次 accept()
返回 conn3。此时 server_socket
还是监听在 8888
端口,但已经产生了多个会话 socket,分别和不同的客户端通信。之后,在 ESTABLISHED
状态下:recv()
从连接中读取字节流、sendall()
发送字节流。最后 conn.close()
关闭会话 socket,触发 TCP 四次挥手。然后 server_socket.close()
关闭监听 socket,停止接受新连接。
客户端(client.py)
1 | # client.py |
同样,首先创建了一个 IPV4+TCP 的 socket,通过 connect()
连接服务端,触发 TCP 三次握手:
- 客户端发 SYN,状态进入
SYN_SENT
; - 服务端(处于
LISTEN
)回 SYN+ACK,状态到SYN_RCVD
; - 客户端回 ACK,双方进入
ESTABLISHED
; - 内核把连接放进服务端的完成队列,服务端
accept()
返回; - 客户端这边的
connect()
也返回,表示握手完成,可以收发数据。
如果服务器没在监听,会得到 “Connection refused”;如果被防火墙拦截,可能超时。握手完成后,才进行应用层数据传输。最后触发四次挥手关闭连接。
和服务端不同的是,客户端只有一个用来通信的 socket,不存在“监听 socket / 通信 socket”的区别。服务端要面对很多客户端,它必须有一个监听 socket,专门用来等别人来连(就像一个“接待处”);同时,每来一个新客户端,生成一个新的会话 socket,专门和该客户端对话。所以服务端需要 server_socket
(监听)+ conn
(会话)。而客户端只会连一个目标地址(例如 127.0.0.1:8888
),它不需要“等待别人来找我”,所以不需要监听 socket。客户端的 socket()
+ connect()
直接建立一个通信通道,得到的这个 socket 就既负责收,也负责发。
WebSocket
WebSocket 虽然名字里带有 Socket,但它是一个应用层协议,定义了客户端与服务端如何基于 TCP 长连接进行全双工通信。它运行在 TCP 之上,也就是说底层依然是 TCP Socket,只是额外规定了一套应用层的帧格式,用来传输文本或二进制数据。
WebSocket 主要是为 Web 浏览器与服务器之间的实时通信设计的,解决了 HTTP 无法主动推送的缺陷。可以这样类比:
- Socket 好比是老式电话机的“听筒和线路接口”;
- TCP 是保证双方通话可靠的“规则”;
- WebSocket 则是双方约定好的“语言和句子格式”。
在 Python 中,可以使用 websockets
库来建立 WebSocket 连接。这个库基于 asyncio
,天然就是协程异步的实现。为什么要异步?因为 WebSocket 的核心价值就是“长连接 + 多客户端”。如果用同步方式,每个连接都要单独占用一个线程或进程,开销过大;而异步方式可以在单线程里高效管理成百上千个连接。
异步上下文管理器
在整理 websockets
之前,先看一下它底层广泛用到的一个机制:异步上下文管理器。
先从一个最小的示例入手:
1 | import asyncio |
上下文管理器主要的一个应用场景就是自动建立连接,并在完成任务后清除连接的。上面异步上下文管理器的写法和我们熟悉的文件读取很像:
1 | with open('file.txt', 'r') as f: |
区别在于:
- 普通
with
在进入和退出时调用__enter__
/__exit__
; - 异步
async with
则会调用__aenter__
/__aexit__
,并且可以在里面使用await
。
这样,就能在连接建立/关闭的地方插入异步 I/O 操作,例如数据库连接、网络套接字等,既能确保资源按作用域释放,又能配合事件循环非阻塞运行。在上面的例子中,当执行 asyncio.run(main())
时,事件循环会把 main()
包装成一个 Task 并运行:
- 执行到
async with FakeDB() as db
→ 调用并await __aenter__()
。 - 进入
__aenter__
后,运行到await asyncio.sleep(1)
,此时 Task 挂起,把控制权交回事件循环。 - 因为这里只有一个任务,没有其他任务可运行,事件循环就空转等待 1 秒后恢复它。
- 返回
self
后进入上下文本体,运行查询逻辑。 - 离开上下文时调用并
await __aexit__
,最终收尾关闭连接。
由于这里只有一个 Task,所以效果上看起来和同步写法差不多。异步真正的优势出现在有多个 Task 并发执行时。比如我们改成并发查询两个“数据库”:
1 | import asyncio |
这里两个 worker
几乎同时运行:当一个任务在 await asyncio.sleep(...)
时,事件循环会切换去运行另一个任务,从而实现了单线程内的并发调度。通过异步上下文管理器,我们看到了 asyncio 的核心模式:把任务拆成协程,遇到 I/O 就主动 await
,事件循环负责调度其他任务继续执行。我们批量产生大量这样的彼此之间独立的协程,就能实现在单线程内高效管理多个并发连接。
接下来,我们就进入正题 —— 看看 websockets
库是如何利用这一模式,把每个 WebSocket 连接都变成一个独立的 Task,从而让一个服务器能同时处理成百上千个客户端。
websockets
简单的例子
服务端
1 | import asyncio |
大体的流程和上面的异步上下文管理器都是一样的,这里的关键就是理解 async with websockets.serve(...)
做了什么。大体上,可以把 websockets.serve(...)
理解为封装了一个 HTTP 服务器 + 升级为 WebSocket 协议的逻辑 + 事件循环并发处理:
调用
asyncio.start_server(...)
:在
localhost:8765
上启动一个 TCP 监听 socket;这个监听 socket 专门接受新连接;
这个监听实际上就是在
ws://localhost:8765
启动了一个服务端口 —— 类似于在 Flask 里用app.run(host, port)
开启一个 HTTP 服务,只不过协议是 WebSocket。
接收到连接时:
普通 TCP → 先读取客户端发来的 HTTP 请求。
如果这个请求里包含
Upgrade: websocket
,就走 HTTP Upgrade 过程:- 校验
Sec-WebSocket-Key
/Sec-WebSocket-Version
等头; - 返回
101 Switching Protocols
响应; - 从 HTTP 切换到 WebSocket 协议(之后收发的是 WebSocket 帧,不再是 HTTP 报文)。
- 校验
创建一个
WebSocketServerProtocol
对象:代表这条 WebSocket 连接;
处理握手后续、消息收发、心跳检测等逻辑;
交给事件循环创建一个 Task:
Task 去运行传入的 handler(例子中的
echo
);handler 的第一个参数
websocket
就是那个WebSocketServerProtocol
对象。
退出 async with 时:
停止监听 socket;
关闭所有现有连接;
优雅清理所有 handler Task。
关于 WebSocketServerProtocol
和 Task,还需要额外说明一下:
在 websockets
库里,每一个 WebSocket 连接都会对应一个 WebSocketServerProtocol
对象,它是一个继承自 asyncio.Protocol
的类实例,由 websockets
内部创建。它是对单条连接的抽象,提供了对 WebSocket 帧的解析和封装。同时提供了包括接收消息、发送消息以及相关的连接状态管理等操作。比如对于这里的 echo
函数,里面的 async for message in websocket
等价于不断 await websocket.recv()
,直到连接关闭。message
可能是 str
(文本帧)或 bytes
(二进制帧),当对端发送 Close 帧或连接断开,循环自动结束。
在这里实现多连接异步,是 websockets
把监听 socket 交给了事件循环注册成了非阻塞的 I/O 监视对象。一旦内核通知可接受新连接,事件循环就会调度响应协程去 accept()
,并为这条连接创建 Task。也就是说 websockets.serve
的启动和调度流程是:
1 | async with websockets.serve(echo, "localhost", 8765): |
__aenter__
内部大致会做:- 创建监听 socket(非阻塞)并
listen(backlog)
; - 调用
asyncio.start_server(...)
把监听 socket 注册到事件循环; - 配置“有新连接时如何处理”(即:接受连接 → 做 HTTP Upgrade → 构造
WebSocketServerProtocol
→loop.create_task(echo(ws, ...))
)。 这些完成后,__aenter__
立即返回,所以马上看到“started”被打印。
- 创建监听 socket(非阻塞)并
await asyncio.Future()
让出控制权,事件循环开始“驻场值班”:- 当有连接进来,循环被内核唤醒 → 调度接受/握手 → 为该连接创建一个新的 Task 去跑
echo(...)
。 - 这些 Task 在
await ws.recv()/send()
处让出,循环就去处理其他连接,实现单线程并发。 - 在这里,
await asyncio.Future()
这和await asyncio.sleep(float("inf"))
的效果是一样的,就相当于让这个协程一直运行不退出。Future
是 asyncio 的底层原语,表示“将来会有一个结果”的占位符。这里构造了一个永远不被完成的 Future,await
它就会永久挂起,等价于让main()
一直不返回。
- 当有连接进来,循环被内核唤醒 → 调度接受/握手 → 为该连接创建一个新的 Task 去跑
连接关闭时,echo
结束;退出 async with
(例如进程信号或手动触发)时,服务器统一收尾关闭。
客户端
1 | import asyncio |
由于 websockets
这个库本身就是围绕 asyncio 设计的,因此不论服务端还是客户端,API 全是异步的。
对于客户端来说,websockets.connect(uri)
是客户端的入口,作用与服务端的 websockets.serve()
对应。它的内部流程和服务端也很像:
创建 TCP 连接:调用
asyncio.open_connection("localhost", 8765)
,这一步底层会:向操作系统请求一个本地临时端口;
发起 TCP 三次握手;
连接成功后,返回一个
StreamReader
/StreamWriter
。
发送 WebSocket 握手(HTTP Upgrade 请求):
其实 WebSocket 一开始还是 HTTP,请求头大致是:
1
2
3
4
5
6GET / HTTP/1.1
Host: localhost:8765
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13服务器收到后,回复
101 Switching Protocols
,表示从 HTTP 切换为 WebSocket 协议。
升级完成,生成
WebSocketClientProtocol
对象:- 这个对象就是
websocket
,它负责:- 管理 TCP 连接;
- 收发 WebSocket 帧;
- 提供
send()
/recv()
协程方法。
- 这个对象就是
建立连接后,await websocket.send()
把字符串编码为 UTF-8,封装成 WebSocket 数据帧(带上帧头+掩码)。之后写入 TCP socket 发送缓冲区,await
可能会在写缓冲区满时让出控制权。await websocket.recv()
等待从 TCP socket 读数据,收到数据后解析 WebSocket 帧,解码为字符串,返回消息。最后 async with
退出,发送 WebSocket Close
帧,关闭底层 TCP 连接,清理任务。
在 Flask 中使用 WebSocket
Flask 本身是一个基于 WSGI(Web Server Gateway Interface)的同步框架,原生只支持 HTTP 请求/响应模型,但我们依然可以通过一些扩展库为 Flask 增加 WebSocket 支持,目前最主流的第三方库是 Flask-SocketIO。这几个概念和库之间的关系是这样的:
TCP 是基础(传输层)。
WebSocket 是应用层协议(RFC 6455),运行在 TCP 之上,提供全双工通信。浏览器原生支持:
1
2
3const ws = new WebSocket("ws://localhost:8765");
ws.onmessage = (e) => console.log(e.data);
ws.send("hello");但是 API 很原始,只有
send
/onmessage
。websockets
库是 Python 的一个第三方库,实现了标准的 WebSocket 协议,轻量、纯粹、完全异步。Socket.IO 是由 Node.js 社区开发的基于 WebSocket 协议扩展的通信框架。WebSocket 本身是一个标准协议,浏览器一旦
new WebSocket("ws://...")
,就是走 WebSocket 握手 + RFC 6455 数据帧,没有别的选项。但是 Socket.IO 为了兼容不支持 WebSocket 的旧浏览器 / 网络环境,设计了一个 “传输协商” 机制:客户端第一次连接时,不是直接发
WebSocket
,而是发一个 HTTP 请求:1
GET /socket.io/?EIO=4&transport=polling&t=123456
这里
transport=polling
表示:先用 HTTP 长轮询建立初始连接。长轮询是一种基于 HTTP 的伪实时通信方式,普通的 HTTP 请求/响应是短链接,客户端发请求,服务器马上回应,连接关闭。但是长轮询改了一下逻辑:客户端首先发送一个 HTTP 请求给服务器,服务器不立即返回,而是挂起请求,直到有新数据才返回响应。客户端一旦收到响应,立即再发起一个新的请求。这样,就形成了一个请求-响应-再请求的循环,看起来就像实时通信一样。
服务端回复一段 JSON,告诉客户端支持哪些传输方式(polling、websocket 等)。
客户端尝试升级,如果环境支持 WebSocket,就会再发一个 WebSocket 请求:
1
GET /socket.io/?EIO=4&transport=websocket&t=654321
服务器返回
101 Switching Protocols
,连接升级为 WebSocket。不过,这并不是标准的 WebSocket 帧,Socket.IO 在 WebSocket 帧里又封装了一层事件机制。如果一个原生的 WebSocket 客户端去直接连 Socket.IO 服务,是无法通信的,因为他们在应用层的数据帧格式不同。如果环境不支持 WebSocket(老 IE / 被防火墙屏蔽),就会一直用 HTTP 长轮询。
Socket.IO 是跨语言的,JS(socket.io 是服务端实现,socket.io-client 是客户端实现)、Python(python-socketio)等都有对应的实现。所以总的来说,Socket.IO 基于 WebSocket/HTTP,带私有的封装规则。所以 Socket.IO ≠ WebSocket 协议,它是在 WebSocket 之上实现的一个私有通信层。
Flask-SocketIO 是 Flask 的扩展库,基于 Flask + python-socketio 封装。所以从上面的讨论也能看到,Flask-SocketIO 启动的服务端是没法用
websockets
客户端直接访问的,但是可以在浏览器里通过 socket.io 访问。
graph TD subgraph 网络协议 TCP["TCP 传输层"] WS["WebSocket 协议 (RFC 6455)"] HTTP["HTTP(S)"] end subgraph 高层封装 SIO["Socket.IO 框架
(事件机制 + 房间 + 消息格式)"] end subgraph Python 实现 PY_WS["websockets 库
(原生 WebSocket 客户端/服务端)"] FLASK_SIO["Flask-SocketIO
(服务端实现,基于 Socket.IO)"] PY_SIO["python-socketio
(客户端/服务端实现)"] end %% 协议关系 TCP --> WS TCP --> HTTP WS --> PY_WS WS --> SIO HTTP --> SIO SIO --> PY_SIO PY_SIO --> FLASK_SIO
服务端
1 | from flask import Flask, render_template |
对于一个正常的 Flask 应用,一个 URL 对应一个路由函数,比如 /users
可能对应 get_users()
。但是 Flask-SocketIO 不是基于 URL 路由,而是基于事件。客户端先和服务端建立一个 WebSocket 连接,之后所有交互都在这个长连接里完成。每条消息通过事件名来区分,比如 message
事件、chat
事件等。事件有点像消息队列:生产者往某个主题里投递消息,消费者订阅主题,收到消息就执行相关操作(回调)。不过和消息队列的不同点是,Socket.IO 是点对点/房间广播。消息不会存储,断开连接就丢了,不会持久化。
同时,Flask-SocketIO 应用也不能直接用原始的 Flask 应用和 app.run()
。首先,需要把普通的 Flask 应用包装成一个支持 Socket.IO 的服务端:
1 | socketio = SocketIO(app, cors_allowed_origins="*") |
之后,需要使用 socketio.run()
来运行,它会根据环境选择合适的异步服务器并启用 Socket.IO 协议栈:
- 安装了 eventlet → 用 eventlet 服务器(协程/greenlet 并发,支持 WS)
- 安装了 gevent/gevent-websocket → 用 gevent pywsgi(也支持 WS)
- 都没有 → 退回到线程模式(仅适合开发,性能有限)
同时它会挂载好 /socket.io/
端点、握手与心跳、升级等逻辑。当然,也可以用 Gunicorn 来运行:
1 | gunicorn -k eventlet -w 1 app:app |
只要 worker 是支持异步的 eventlet/gevent。Gunicorn 提供了进程管理、超时、日志、配置、优雅重载、与反向代理配合等生产特性;socketio.run()
更像便捷启动器/开发用。在 Gunicorn 的 worker 大于1的时候,在封装 app 的时候可以加一个消息队列来做跨进程通信:
1 | socketio = SocketIO( |
方便不同 worker 进程之间彼此看得见对方的连接。message_queue
一配置,Flask‑SocketIO 就会把广播通过 Redis 同步给所有 worker。
对于 Socket.IO 来说,客户端或者服务端都可以用 emit(event, data)
来往某个事件里发消息。这里的 send(data)
:是 emit("message", data)
的简写,事件名固定就是 "message"
。 对端如果注册了 @socketio.on(event)
或者 socket.on(event, handler)
,就能收到并处理。比如在上面的 Python 代码中,@socketio.on("message")
的含义就是,一旦收到了 message
事件的消息,就执行 handle_message
这个函数。在其他语言比如 JavaScript 中,语法也都比较类似:
1 | socket.on("message", (data) => console.log("收到 send:", data)); |
客户端
我们用浏览器来测试上面的 Flask-SocketIO 服务端,在浏览器里,访问 about:blank
来打开一个空页面。about:
是一类浏览器内部伪协议,用来访问内置的特殊页面,比如 about:blank
是空页面,在 Chrome 里 about:version
是浏览器版本信息等。页面打开后,通过 F12 开发者模式打开控制台,执行:
1 | // 1. 加载 Socket.IO |
其中:
1 | const script = document.createElement('script'); |
创建了一个 <script>
标签,指向 Socket.IO 官方 CDN 的客户端库。
script.onload
事件会在外部脚本下载并执行完成后触发。<script>
、<img>
、<iframe>
等元素都有 onload
事件:当资源加载完成时,浏览器会触发,这是浏览器在 DOM(Document Object Model)元素上实现的事件属性。浏览器 DOM 规范里规定了哪些元素有 onload
事件,JS 用函数赋值 element.onload = function(){}
来挂接回调。
在具体的回调函数中,我们使用 io()
连接到运行在本地5000端口的 Socket.IO 服务器。如果连接成功,浏览器就会和服务端建立 WebSocket 或者轮询长连接。socket.on('connect', ...)
当客户端成功和服务器握手后立即执行后面的函数。在这里,这个函数就是 socket.emit('message', { ... })
,发送一条名为 "message"
的事件,附带 JSON 数据 { text: '来自控制台的消息' }
。
=>
是定义函数的一种简写形式,相当于:
1 | // 箭头函数写法 |
可以把它理解为 ()
部分是参数列表,=>
表示返回这个函数体,{}
是函数体。
然后,通过 socket.on('message', (data) => {console.log('服务器回复:', data);});
监听 "message"
事件。一旦服务器通过 socket.emit('message', ...)
回复消息,这里就会触发,并打印数据。
最后,通过 DOM 把 <script>
插入页面,让浏览器去下载并执行它,整个逻辑就会运行起来。
小结
从 Socket 到 WebSocket,再到 Socket.IO,我们可以看到网络通信在不同抽象层次上的演化:
- Socket 是操作系统提供的编程接口,本质上只是读写字节流的“插口”。它基于 TCP/UDP,为应用层提供了最底层的通信能力。
- WebSocket 则是应用层协议,运行在 TCP 之上,定义了消息帧格式,支持 全双工、长连接、轻量头部,解决了 HTTP 无法主动推送的限制,非常适合聊天室、实时推送、在线协作等场景。
- Socket.IO 更进一步,在 WebSocket 协议之上又封装了事件机制、房间广播和兼容降级策略(比如轮询),使开发者能以更简单的方式处理复杂的实时通信逻辑。不过它与原生 WebSocket 不完全兼容,本质上是一个 基于 WebSocket/HTTP 的私有通信层。
对于 Python 开发者来说,可以使用 websockets
库来实现原生 WebSocket 服务,也可以使用 Flask-SocketIO 这样的扩展来获得 Socket.IO 的便利特性。不同选择,取决于你对 性能、兼容性 以及 生态支持 的需求。
如果把通信比作“打电话”:
- TCP 像是电话线路,保证你能可靠接通。
- Socket 是听筒,让你能接入线路收发声音。
- WebSocket 规定了双方说话的语言和句子格式。
- Socket.IO 则在此基础上提供了会议模式、群聊功能和“翻译”服务,方便不同环境下都能顺畅沟通。