手搓聊天室1.0.0----基于ws和protobuf协议
前言
本系列文章本旨为讲解如何利用websocket和protobuf手搓聊天室,并如何破解这种加密,分为三阶段,第一阶段讲解websocket和protobuf相关知识,并手搓简单Chatbox(也即本文),第二阶段为如何讲解实现websocket爬虫(讲解抖音直播弹幕,b站直播弹幕等),第三阶段为不断完善Chatbox,形成可投入产品。代码讲放在https://github.com/mingheyan/Chatbox,分为不同阶段不同版本
先看本文效果
websockets
WebSocket 协议最初是为了改善 Web 应用中实时通信的性能而设计的。传统的 HTTP 请求-响应模型在处理实时数据时效率较低,因为每次通信都需要建立和关闭连接。而 WebSocket 提供了一种持久连接的方式,使得客户端和服务器可以随时发送和接收消息。
WebSocket 是一种基于 TCP 的双向通信协议,允许客户端和服务器之间进行实时、全双工的数据传输,广泛应用于在线聊天、实时游戏、物联网等领域。以下是关于 WebSocket 协议的详细讲解,包括其工作原理、客户端(JavaScript)和服务端(Python)的实现方式。
下载pip install websockets
在 Python 中,websockets
是一个常用的库,用于实现 WebSocket 协议的服务器和客户端。以下是 websockets
库中一些常用的方法和功能:
1. 服务器端方法
-
websockets.serve()
用于创建 WebSocket 服务器。它接受一个处理函数(用于处理 WebSocket 连接)、主机地址和端口号等参数。示例:
import asyncio import websockets async def handler(websocket, path): async for message in websocket: await websocket.send(f"Echo: {message}") start_server = websockets.serve(handler, "localhost", 8765) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
-
websocket.recv()
用于接收客户端发送的消息。它是一个异步方法,返回接收到的消息。 -
websocket.send()
用于向客户端发送消息。它也是异步方法。 -
websocket.close()
用于显式关闭 WebSocket 连接。
2. 客户端方法
-
websockets.connect()
用于建立与 WebSocket 服务器的连接。示例:
import asyncio import websockets async def client(): async with websockets.connect("ws://localhost:8765") as websocket: await websocket.send("Hello, server!") response = await websocket.recv() print(f"Received: {response}") asyncio.run(client())
-
websocket.recv()
和websocket.send()
与服务器端类似,客户端也可以使用这些方法接收和发送消息。
3. 其他功能
-
广播消息
服务器可以维护一个客户端连接集合,并向所有连接的客户端广播消息。示例:
connected_clients = set() async def handler(websocket, path): connected_clients.add(websocket) try: async for message in websocket: await broadcast(message) finally: connected_clients.remove(websocket) async def broadcast(message): for client in connected_clients: await client.send(message)
-
心跳机制
WebSocket 协议支持心跳机制(ping/pong),websockets
库会自动处理这些机制,无需手动干预。
下面我们基于此来进行实现chatbox
JavaScript 客户端实现
在浏览器中,可以通过原生的 WebSocket
API 建立 WebSocket 连接。以下是一个简单的实现示例:
import asyncio
import websockets
# 存储所有连接的客户端
Connections = set()
# 广播函数
async def broadcast(message):
if Connections:
await asyncio.wait([conn.send(message) for conn in Connections])
# 处理客户端连接
async def handler(websocket, path):
# 添加新连接到集合
Connections.add(websocket)
try:
async for message in websocket:
# 接收到消息后进行广播
await broadcast(message)
finally:
# 客户端断开连接时移除
Connections.remove(websocket)
# 启动 WebSocket 服务
async def main():
async with websockets.serve(handler, "localhost", 8080):
await asyncio.Future() # 保持服务运行
asyncio.run(main())
Python 服务端实现
在 Python 中,可以使用 websockets
库来创建 WebSocket 服务端。以下是一个简单的服务端实现示例:
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('连接到服务器');
// 发送一条消息到服务器
socket.send('Hello, Server!');
};
socket.onmessage = (event) => {
console.log('从服务器收到消息:', event.data);
// 可以将消息显示在页面上
document.getElementById('messages').innerText += event.data + '\n';
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
socket.onclose = () => {
console.log('与服务器断开连接');
};
基于此,我们写了个基本的聊天室,来帮助我们深入了解websocket的过程,源代码在https://github.com/mingheyan/Chatbox:
建立一个输入用户名的弹窗,其用户名自动存入
Connections = set()中,之后利用广播机制发送给客户端。
客户端
服务端
建立一个发送消息的组件,发送的消息使用广播机制,我们打开F12,到ws模块里面,如下图,当我们发送消息的时候,客户端会同步呈现,ws模块里面是我们发送的信息
基于此我们便直接答完了一个简单自定义聊天室1.0,在1.0版本,我们实现了
- 简单的前端界面(有点丑),
- 实现了用户组,和实时界面功能
- 使用json定义数据结构
是不是非常简单捏
那我们加大点难度,了解protobuf模块,并使用protobuf来定义数据结构
Protocol Buffers(简称 Protobuf)是一种语言无关、平台无关的序列化数据格式,由 Google 开发并开源。它主要用于高效地存储和传输结构化数据,广泛应用于网络通信、数据存储、微服务通信等场景。以下是关于 Protobuf 的详细讲解,包括其设计原理、使用方法、以及在不同语言中的实现。
1. Protobuf 的设计原理
1.1 为什么需要 Protobuf?
在开发中,我们经常需要将数据序列化(将数据结构或对象状态转换为可存储或传输的格式)和反序列化(将序列化的数据恢复为原始数据结构)。常见的序列化方式包括 JSON、XML 等,但它们存在以下问题:
- 性能问题:JSON 和 XML 的序列化和反序列化速度较慢,且数据体积较大。
- 语言依赖:不同语言对序列化格式的支持程度不同,难以实现跨语言通信。
- 版本兼容性:当数据结构发生变化时,老版本的代码可能无法解析新版本的数据。
Protobuf 通过以下方式解决了这些问题:
- 高效性:Protobuf 的序列化速度比 XML 快 20-100 倍,数据体积比 JSON/XML 小 3-10 倍。
- 语言无关性:Protobuf 提供了多种语言的实现,包括 C++、Java、Python、Go、JavaScript 等。
- 版本兼容性:Protobuf 允许开发者在不破坏现有代码的情况下,对数据结构进行扩展。
1.2 基本概念
.proto
文件:定义了数据的结构。开发者通过.proto
文件声明消息类型(类似于类的定义)。- 消息(Message):Protobuf 中的数据结构,类似于其他语言中的类或结构体。
- 字段(Field):消息中的属性,每个字段都有一个名称、类型和编号。
- 字段编号:字段编号是消息定义中的唯一标识符,用于在序列化和反序列化时区分字段。
1.3 数据类型
Protobuf 支持多种数据类型,包括:
- 标量类型:如
int32
、float
、double
、string
等。 - 复合类型:如
message
(自定义消息类型)。 - 特殊类型:如
enum
(枚举类型)、map
(键值对类型)。
2. Protobuf 的使用方法
2.1 定义 .proto
文件
假设我们正在开发一个用户管理系统,需要定义用户信息和用户列表。以下是 .proto
文件的示例:
syntax = "proto3"; // 指定 Protobuf 语法版本
package user; // 定义包名,用于避免命名冲突
// 定义用户信息
message User {
int32 id = 1; // 用户ID
string name = 2; // 用户姓名
string email = 3; // 用户邮箱
repeated string phone = 4; // 用户电话号码(可重复字段)
}
// 定义用户列表
message UserList {
repeated User users = 1; // 用户数组
}
2.2 编译 .proto
文件
Protobuf 提供了一个编译器 protoc
,用于将 .proto
文件编译为特定语言的代码。例如:
- Python:生成
.py
文件。 - Java:生成
.java
文件。 - C++:生成
.h
和.cpp
文件。
以 Python 为例,编译命令如下:
protoc --python_out=. user.proto
这将生成一个 user_pb2.py
文件,其中包含了用户定义的消息类型和相关方法。
2.3 使用生成的代码
以下是 Python 中使用生成的代码的示例:
from user_pb2 import User, UserList
# 创建一个用户对象
user = User(id=1, name="Alice", email="alice@example.com")
user.phone.append("123-456-7890")
user.phone.append("098-765-4321")
# 创建用户列表
user_list = UserList()
user_list.users.append(user)
# 序列化为二进制数据
serialized_data = user_list.SerializeToString()
print("Serialized Data:", serialized_data)
# 反序列化为对象
new_user_list = UserList()
new_user_list.ParseFromString(serialized_data)
for user in new_user_list.users:
print(f"User ID: {user.id}, Name: {user.name}, Email: {user.email}, Phones: {user.phone}")
3. Protobuf 的高级特性
3.1 枚举类型
Protobuf 支持定义枚举类型,用于表示一组固定的常量值。例如:
enum Role {
ADMIN = 0;
USER = 1;
GUEST = 2;
}
message User {
int32 id = 1;
string name = 2;
Role role = 3; // 使用枚举类型
}
3.2 嵌套消息
可以在消息中嵌套其他消息类型。例如:
message Address {
string street = 1;
string city = 2;
string state = 3;
}
message User {
int32 id = 1;
string name = 2;
Address address = 3; // 嵌套消息
}
3.3 重复字段
使用 repeated
关键字可以定义可重复字段,类似于数组或列表。例如:
message User {
int32 id = 1;
string name = 2;
repeated string phone = 3; // 可重复字段
}
3.4 默认值
Protobuf 为标量字段提供了默认值。例如:
- 数字类型的默认值为
0
。 - 字符串类型的默认值为
""
。 - 枚举类型的默认值为第一个枚举值。
3.5 版本兼容性
Protobuf 设计了强大的版本兼容性机制:
- 向后兼容:添加新字段时,旧版本的代码仍然可以正常解析数据。
- 向前兼容:删除字段或修改字段编号时,新版本的代码可以通过默认值处理旧数据。
4. Protobuf 的性能优势
4.1 数据体积小
Protobuf 的序列化数据体积比 JSON 和 XML 小得多。例如:
- JSON:
{"id":1,"name":"Alice","phones":["123-456-7890","098-765-4321"]}
- Protobuf:二进制数据,体积更小。
4.2 序列化和反序列化速度快
Protobuf 的序列化和反序列化速度比 JSON 和 XML 快 20-100 倍,适合高性能场景。
4.3 语言无关性
Protobuf 提供了多种语言的实现,支持跨语言通信。例如:
- Python:
user_pb2.py
- Java:
User.java
- C++:
User.h
和User.cpp
基于此我们对我们的聊天室使用protobuf进行存储数据
首先创建我们的存储文件格式:
chat.proto
客户端
在客户端引入protobuf文件
配置相关
发送逻辑改为
其中WebSocketMessage.encode(message).finish() 是 Protobuf 中用于序列化消息的方法调用
这样发送的数据变为二进制,打开F12查看,发现这里不再是明文数据
服务端
服务端可相对应的改
其中
data = WebSocketMessage()
创建一个新的 WebSocketMessage Protobuf 对象
data.ParseFromString(message)
ParseFromString 将二进制消息数据解析为 Protobuf 对象
message 是从客户端接收到的原始二进制数据
data.type == MessageType.CHAT_MESSAGE:
检查消息类型是否为聊天消息
SerializeToString()
将 Protobuf 对象转换回二进制格式,传给客户端广播
这样既将存储手段变为protobuf,并且websocket里面传输的数据变成二进制
在2.0版本,我们使用
- 主要使用protobuf来定义数据
- 间接实现了ws加密
下文我们将进行抖音直播间弹幕逆向和b站直播弹幕逆向
项目地址:https://github.com/mingheyan/Chatbox
将持续更新