传统的 HTTP 请求通常是一次请求一次响应的,而 WebSocket 则可以建立一个持久连接,允许服务器即时向客户端推送数据,同时也可以接收客户端发送的数据。WebSocket 相比于传统的轮询或长轮询方式,能够显著减少网络流量和延迟,提高数据传输的效率和速度。它对实时 Web 应用程序和在线游戏的开发非常有用;
websocket 基础原理
WebSocket 是 HTML5 开始推出的基于 TCP 协议的双向通信协议,其优势在于与 HTTP 协议兼容、开销小、通信高效。WebSocket 让客户端和服务器之间建立连接,并通过这个持久连接实时地进行双向数据传
WebSocket 最主要的特点就是建立了一个可持久化的 TCP 连接,这个连接会一直保留,直到客户端或者服务器发起中断请求为止。WebSocket 通过 HTTP/1.1 协议中的 Upgrade 头信息来告诉服务器,希望协议从 HTTP/1.1 升级到 WebSocket 协议
WebSocket 建立在 HTTP 协议之上,所有的 WebSocket 请求都会通过普通的 HTTP 协议发送出去,然后在服务器端根据 HTTP 协议识别特定的头信息 Upgrade,服务端也会判断请求信息中 Upgrade 是否存在。 这里面 HTTP 是必不可少的,不然 WebSocket 根本无法建立。特别的,WebSocket 在握手时采用了 Sec-WebSocket-Key 加密处理,并采用 SHA-1 签名。
一旦建立了 WebSocket 连接,客户端和服务器端就可以互相发送二进制流或 Unicode 字符串。所有的数据都是经过 mask 处理过的,mask 的值是由服务器端随机生成的。在数据进行发送之前,必须先进行 mask 处理,这样可以有效防止数据被第三方恶意篡改。
WebSocket 与 Socket、TCP、HTTP 的关系及区别
power by openai
WebSocket、Socket、TCP 和 HTTP 都涉及到计算机网络中的数据传输,但它们的协议、用途、工作方式等方面有显著的差异。下面是它们之间的关系和主要区别:
1. WebSocket
定义:WebSocket 是一种基于 TCP 协议的全双工通信协议,它使得客户端和服务器之间可以建立一个持久的、双向的通信通道。WebSocket 允许客户端和服务器在同一个连接上进行实时数据交换,适合用于需要频繁交互的应用(如聊天应用、在线游戏等)。
工作方式:WebSocket 在初始连接时通过 HTTP 协议进行握手,但之后会升级为 WebSocket 协议,建立一个持久的 TCP 连接。这意味着一旦连接建立,数据可以在客户端和服务器之间双向流动,而无需每次请求都重新建立连接。
优点:
双向通信:客户端和服务器都可以主动发送数据。
低延迟:避免了 HTTP 请求-响应的开销,减少了频繁建立连接的成本。
实时性:适合实时应用,如股票行情、在线游戏、聊天系统等。
使用场景:实时通信、在线游戏、在线协作、即时通知等。
2. Socket
定义:Socket 是一种提供数据传输的接口,是程序与网络之间通信的基础。它是操作系统层面的抽象,能够让应用程序进行网络通信。Socket 可以支持多种协议(如 TCP、UDP 等),并提供了一种抽象接口,让开发者无需关注底层的网络协议。
工作方式:Socket 本身并不是一个协议,而是一个通信接口。它允许应用程序通过 IP 地址和端口进行数据发送和接收。Socket 本身可以基于不同的协议实现,如 TCP 或 UDP。
与 WebSocket 的关系:WebSocket 也是通过 Socket 来实现通信的。WebSocket 连接实际上是通过 TCP Socket 建立的,它使用 TCP 作为传输层协议,且通过特定的握手协议与 HTTP 进行兼容。
使用场景:一般的客户端-服务器通信,TCP/UDP 应用开发等。
3. TCP (Transmission Control Protocol)
定义:TCP 是一种面向连接的、可靠的传输协议。它在数据传输之前需要通过三次握手建立连接,并且在传输过程中提供错误检测、流量控制、数据重传等机制,确保数据正确、可靠地到达目标。
工作方式:TCP 通过端口号来识别通信的应用进程,并通过 IP 地址和端口号来建立连接。它确保数据包的顺序和完整性,通过确认应答、重传等机制实现可靠性。
与 WebSocket 的关系:WebSocket 使用 TCP 作为底层传输协议。WebSocket 连接在建立时,实际上就是通过 TCP 来进行数据传输的,因此 WebSocket 能够利用 TCP 提供的可靠性和连接保持特性。
使用场景:需要可靠数据传输的场合,如文件传输、HTTP 请求、数据库通信等。
4. HTTP (HyperText Transfer Protocol)
定义:HTTP 是一种无状态、请求-响应式的应用层协议。客户端(如浏览器)向服务器发送请求,服务器响应数据。HTTP 是 Web 应用最常用的协议,用于浏览器和 Web 服务器之间的通信。
工作方式:HTTP 是基于 TCP 协议的,客户端和服务器之间通过 HTTP 请求-响应模式交换数据。每次客户端发送请求时,都会重新建立一个连接(除非使用 HTTP/2 的连接复用功能)。这种请求-响应模式适合静态内容和不频繁交互的场合。
与 WebSocket 的关系:WebSocket 在初始连接时使用 HTTP 协议进行握手。在握手成功后,HTTP 连接会“升级”到 WebSocket 协议,从而切换到基于 TCP 的全双工通信。HTTP 和 WebSocket 都使用 TCP 作为底层协议,但 WebSocket 在通信过程中不再遵循 HTTP 的请求-响应模型。
使用场景:Web 浏览、API 调用、文件下载、网页加载等。
关系与区别总结
简要总结:
WebSocket 适合需要双向实时通信的应用,如在线聊天、实时游戏等。它是建立在 TCP 之上的,初始连接使用 HTTP 协议进行握手。
Socket 是用于网络通信的接口,可以基于 TCP 或 UDP 协议实现。
TCP 是传输层协议,提供可靠的数据传输,WebSocket 在底层依赖 TCP。
HTTP 是请求-响应模型的协议,适用于静态页面加载、API 请求等,但不适合实时通信。WebSocket 在初次连接时使用 HTTP 握手。
websocket协议
数据帧
Websocket协议通过序列化的数据帧传输数据。数据封包协议中定义了opcode、payload length、Payload data等字段。其中要求:
1.客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
2.服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。 针对上情况,发现错误的一方可向对方发送close帧(状态码是1002,表示协议错误),以关闭连接。 具体数据帧格式如下图所示:
FIN
标识是否为此消息的最后一个数据包,占 1 bit
RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
其中最重要的字段为opcode(4bit)和MASK(1bit):
MASK值,从客户端进行发送的帧必须置此位为1,从服务器发送的帧必须置为0。如果任何一方收到的帧不符合此要求,则发送关闭帧(Close frame)关闭连接。
opcode的值: 0x1代表此帧为文本数据帧, 0x2代表此帧为二进制数据帧, 0x8为控制帧中的连接关闭帧(close frame), 0x9为控制帧中的Ping帧, 0xA(十进制的10)为控制帧中的Pong帧。
Ping/Pong帧: Ping帧和Pong帧用于连接的保活(keepalive)或者诊断对端是否在线。这两种帧的发送和接收不对WEB应用公开接口,由实现WebSocket协议的底层应用(例如浏览器)来实现它。
MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1
Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:
1.如果其值在0-125,则是payload的真实长度。 2.如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。 3.如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。 这里的长度表示遵循一个原则,用最少的字节表示长度(尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不 允许长度1是126或127,然后长度2是124,这样违反原则。
Payload data
应用层数据
server解析client端的数据
接收到客户端数据后的解析规则如下:
1byte
1bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
2byte
1bit: Mask,1表示该frame包含掩码;0表示无掩码
7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为- 1-4byte掩码循环和数据byte做异或操作。
状态码
1000 连接正常关闭
1001 端点离线,例如服务器down,或者浏览器已经离开此页面
1002 端点因为协议错误而中断连接
1003 端点因为受到不能接受的数据类型而中断连接
1004 保留
1005 保留, 用于提示应用未收到连接关闭的状态码
1006 端点异常关闭
1007 端点收到的数据帧类型不一致而导致连接关闭
1008 数据违例而关闭连接
1009 收到的消息数据太大而关闭连接
1010 客户端因为服务器未协商扩展而关闭
1011 服务器因为遭遇异常而关闭连接
1015 TLS握手失败关闭连接
代码
JavaScript
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
这个代码创建了一个 WebSocket 客户端,连接到 wss://echo.websocket.org。
在连接建立后,它会发送 "Hello WebSockets!" 到服务器。
服务器会返回相同的消息(因为是回声服务器)。
客户端收到消息后,会打印消息并关闭 WebSocket 连接。
python
The default implementation builds upon
asyncio
, Python’s built-in asynchronous I/O library. It provides an elegant coroutine-based API. It’s ideal for servers that handle many client connections.The
threading
implementation is a good alternative for clients, especially if you aren’t familiar withasyncio
. It may also be used for servers that handle few client connections.The Sans-I/O implementation is designed for integrating in third-party libraries, typically application servers, in addition being used internally by websockets.
asyncio
#!/usr/bin/env python
"""Echo server using the asyncio API."""
import asyncio
import datetime
from websockets.asyncio.server import serve
async def echo(websocket):
"""
async for message in websocket:异步接收 WebSocket 客户端发送的每一条消息。WebSocket 连接是双向的,这里我们接收客户端发来的消息,并通过 message 变量处理。
await websocket.send(message):接收到消息后,立即将该消息发送回客户端。这意味着服务器是一个“回显”服务器,客户端发送什么消息,服务器就回复相同的消息。
"""
async for message in websocket:
# 获取当前时间
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 创建带有时间的消息
message_with_time = f"{current_time} - {message}"
await websocket.send(message_with_time)
async def main():
async with serve(echo, "localhost", 8765) as server:
print("Serving on", server.sockets[0].getsockname())
await server.serve_forever()
if __name__ == "__main__":
print("Serving on port 8765")
asyncio.run(main())
#!/usr/bin/env python
"""Client using the asyncio API."""
import asyncio
from websockets.asyncio.client import connect
async def hello():
async with connect("ws://localhost:8765") as websocket:
await websocket.send("Hello world!")
message = await websocket.recv()
print(message)
if __name__ == "__main__":
asyncio.run(hello())
threading
#!/usr/bin/env python
"""Echo server using the threading API."""
import datetime
import time
from websockets.sync.server import serve
def echo(websocket):
for message in websocket:
time.sleep(10)
# 获取当前时间
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 创建带有时间的消息
message_with_time = f"{current_time} - {message}"
websocket.send(message_with_time)
def main():
with serve(echo, "localhost", 8765) as server:
server.serve_forever()
if __name__ == "__main__":
main()
# client.py
from websockets.sync.client import connect
import threading
import time
def hello(client_id):
print(f"Client {client_id} starting at {time.strftime('%H:%M:%S')}")
with connect("ws://localhost:8765") as websocket:
websocket.send(f"Hello from client {client_id}!")
message = websocket.recv()
print(f"Client {client_id} received at {time.strftime('%H:%M:%S')}: {message}")
def run_clients():
threads = []
for i in range(3): # Testing with 3 clients
thread = threading.Thread(target=hello, args=(i,))
threads.append(thread)
thread.start()
time.sleep(1) # Start clients with slight delay
for thread in threads:
thread.join()
if __name__ == "__main__":
run_clients()
# client_no_threading.py
from websockets.sync.client import connect
import time
def hello(client_id):
print(f"Client {client_id} starting at {time.strftime('%H:%M:%S')}")
with connect("ws://localhost:8765") as websocket:
websocket.send(f"Hello from client {client_id}!")
message = websocket.recv()
print(f"Client {client_id} received at {time.strftime('%H:%M:%S')}: {message}")
def run_clients():
# Sequential execution without threading
for i in range(3):
hello(i)
if __name__ == "__main__":
run_clients()
独立连接:当两个客户端几乎同时发起连接请求时,服务器会为每个客户端创建一个新的WebSocket连接。这意味着每个客户端都有自己的通信通道,服务器可以独立地与每个客户端进行交互。
同步处理 vs 并发连接:尽管服务器对每个连接的消息处理是同步且顺序进行的(即在处理完当前连接的消息之前不会开始处理下一个连接的消息),但这并不妨碍它接受新的连接。因此,如果两个客户端几乎同时发送消息,服务器实际上是在两个不同的连接上工作,而不是试图在一个连接上同时处理两条消息
应用:fastapi
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://localhost:8002/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""
@app.get("/")
async def get():
return HTMLResponse(html)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
评论区