Compare commits

..

5 Commits

Author SHA1 Message Date
897f124455 更新 API3.1.3.md 2025-06-14 03:25:22 +08:00
d5ef30678b 上传文件至 / 2025-06-14 03:25:05 +08:00
a4e62e31bd 添加 API3.1.2.md 2025-06-14 03:23:50 +08:00
dzy
52363b6535 上传文件至 ChatServer 2025-06-07 11:34:23 +08:00
cb232564d5 更新 API文档3.0.md 2025-06-05 16:58:07 +08:00
4 changed files with 994 additions and 2 deletions

338
API3.1.3.md Normal file
View File

@ -0,0 +1,338 @@
## HTTP API
### 1. 用户注册
- **URL**: `/api/register`
- **方法**: `POST`
- **请求头**: `Content-Type: application/json`
- **请求体**:
```json
{
"username": "string",
"password": "string",
"avatar": "string" // 可选,默认为'default_avatar.png'
}
```
- **响应**:
- 成功:
```json
{
"type": "register_1",
"error": false,
"message": "User registered successfully"
}
```
- 用户名已存在:
```json
{
"type": "register_0",
"success": false,
"message": "Username already exists"
}
```
- 用户名格式错误返回状态码400:
```json
{
"type": "register_-2",
"success": false,
"message": "Invalid username. Must be 2-20 characters, start with a letter or symbol, and contain only letters, numbers, or symbols: _!@#$%^&+-~?"
}
```
- 头像格式错误(仅允许.png或.jpg:
```json
{
"type": "register_0",
"success": False,
"message": "Invalid avatar format. Only .png or .jpg allowed"
}
```
- 其他错误(如数据库错误):
```json
{
"type": "register_-1",
"error": true,
"message": "错误详情"
}
```
### 2. 用户登录
- **URL**: `/api/login`
- **方法**: `POST`
- **请求头**: `Content-Type: application/json`
- **请求体**:
```json
{
"username": "string",
"password": "string"
}
```
- **响应**:
- 成功:
```json
{
"type": "login_1",
"status": "success",
"token": "string", // 令牌,用于后续请求
"avatar": "string" // 用户头像的URL
}
```
- 账号已登录返回状态码409:
```json
{
"type": "login",
"status": "error_0",
"message": "Account already logged in"
}
```
- 凭证错误:
```json
{
"type": "login_0",
"status": "error",
"message": "Invalid credentials"
}
```
### 3. 发送聊天消息
- **URL**: `/api/chat`
- **方法**: `POST`
- **请求头**: `Content-Type: application/json`
- `Authorization: <token>` // 登录时获取的令牌
- **请求体**:
```json
{
"message": "string"
}
```
- **响应**:
- 成功:
```json
{
"type": "chat",
"status": "success"
}
```
- 令牌无效或缺失返回状态码401:
```json
{
"type": "chat",
"status": "error"
}
```
### 4. 获取头像
- **URL**: `/avatar/<username>/<filename>`
- **方法**: `GET`
- **响应**:
- 返回用户头像文件(如果存在),否则返回默认头像。
## Socket API
Socket服务器运行在`8889`端口。客户端连接后可以发送JSON格式的消息。每个消息必须包含`type`字段,表示操作类型。
### 消息格式
所有消息均为JSON格式编码为UTF-8字符串发送。
### 1. 注册
- **请求**:
```json
{
"type": "register",
"username": "string",
"password": "string",
"avatar": "string" // 可选
}
```
- **响应**:
- 成功:
```json
{
"type": "register_1",
"error": false,
"message": "User registered successfully"
}
```
- 用户名已存在:
```json
{
"type": "register_0",
"success": false,
"message": "Username already exists"
}
```
- 用户名格式错误:
```json
{
"type": "register_-2",
"success": false,
"message": "Invalid username format"
}
```
- 头像格式错误:
```json
{
"type": "register_-3",
"success": false,
"message": "Invalid avatar format. Only .png or .jpg allowed"
}
```
### 2. 登录
- **请求**:
```json
{
"type": "login",
"username": "string",
"password": "string"
}
```
- **响应**:
- 成功:
```json
{
"type": "login",
"status": "success",
"message": "Login successful",
"token": "string", // 用于后续操作的令牌
"username": "string", // 登录的用户名
"avatar": "string" // 头像URL
}
```
- 账号已登录:
```json
{
"type": "login",
"status": "error_-1",
"message": "Account already logged in"
}
```
- 凭证错误:
```json
{
"type": "login",
"status": "error_0",
"message": "Invalid credentials"
}
```
### 3. 发送聊天消息
- **请求**:
```json
{
"type": "chat",
"token": "string", // 登录时获取的令牌
"message": "string" // 聊天内容
}
```
- **响应**:
- 成功:
```json
{
"type": "chat",
"status": "success"
}
```
- 令牌无效:
```json
{
"type": "chat",
"status": "error_It",
"message": "Invalid token"
}
```
- 未登录:
```json
{
"type": "chat",
"status": "error_Nli",
"message": "Not logged in"
}
```
### 4. 心跳检测
- **请求**:
```json
{
"type": "heartbeat",
"token": "string"
}
```
- **响应**:
- 成功:
```json
{
"type": "heartbeat",
"status": "success"
}
```
- 失败:
```json
{
"type": "heartbeat",
"status": "error"
}
```
## 错误码说明
### 注册相关
- `register_1`: 注册成功
- `register_0`: 用户名已存在
- `register_-1`: 数据库错误
- `register_-2`: 用户名格式错误
- `register_-3`: 头像格式错误仅Socket
### 登录相关
- `login_1`: 登录成功HTTP
- `login_0`: 凭证错误HTTP或登录失败Socket中为`error_0`
- `error_-1`Socket: 账号已登录
### 聊天相关
- `error_Mt`: 令牌缺失Socket
- `error_It`: 令牌无效Socket
- `error_Nli`: 未登录Socket
## 用户名校验规则
- 长度2-20个字符
- 只能包含字母、数字以及特定符号:`_!@#$%^&+-~?`
- 不能以数字开头
## 头像规则
- 头像文件名必须以`.png``.jpg`结尾。
- 每个用户有专属的头像目录,位于`avatar/<username>`下。
- 默认头像为`avatar/default/default_avatar.png`
## 连接管理
- 每个用户只能有一个活跃连接。如果尝试登录时已有活跃连接则拒绝登录HTTP返回409Socket返回错误
- 服务器会定期5分钟检查不活跃的连接并清理。
- 客户端应每5分钟发送一次心跳包以保持连接活跃。
## 注意事项
- 令牌有效期为1小时每次使用会刷新有效期。
- 头像的获取通过HTTP GET请求路径为`/avatar/<username>/<filename>`
请根据实际情况调整使用。

View File

@ -1,5 +1,4 @@
```markdown
markdown
# API 文档
## 基础信息

468
CS3.1.3.py Normal file
View File

@ -0,0 +1,468 @@
import threading
import json
from flask import Flask, jsonify, request, send_from_directory
import sqlite3
import socket
import secrets
import time
import os
import logging
import re # 添加正则表达式模块
# 在文件顶部添加常量定义
AVATAR_BASE_DIR = "avatar" # 头像存储基础目录
DEFAULT_AVATAR = "default_avatar.png" # 默认头像文件名
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
socket_server = socket.socket()
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 修改数据结构:使用用户名作为主键
active_connections = {} # {username: {'conn': conn, 'ip': ip, 'last_active': timestamp}}
chat_connections = [] # 所有活跃连接列表
tokens = {} # 令牌管理
def get_db_connection():
conn = sqlite3.connect("usr.db")
conn.row_factory = sqlite3.Row
return conn
def isuserxist(name):
cn = get_db_connection()
csr = cn.cursor()
csr.execute('SELECT * FROM users WHERE name = ?', (name,))
rst = csr.fetchone()
cn.close()
return rst is not None
def ispsswdright(name, passwd):
cn = get_db_connection()
csr = cn.cursor()
csr.execute("SELECT passwd FROM users WHERE name = ?", (name,))
result = csr.fetchone()
cn.close()
return result and result[0] == passwd
# 用户名验证函数
def validate_username(username):
"""验证用户名是否符合规则"""
# 长度在2-20个字符
if len(username) < 2 or len(username) > 20:
return False
# 只能包含特定字符字母、数字、_!@#$%^&+-~?
if not re.match(r'^[a-zA-Z0-9_!@#$%^&+\-~?]+$', username):
return False
# 不能以数字开头
if username[0].isdigit():
return False
return True
# 修改获取头像函数
def get_avatar(username):
cn = get_db_connection()
csr = cn.cursor()
csr.execute("SELECT avatar FROM users WHERE name = ?", (username,))
result = csr.fetchone()
cn.close()
if result:
avatar_file = result[0]
# 返回HTTP URL格式的头像路径
return f"/avatar/{username}/{avatar_file}"
# 返回默认头像URL
return f"/avatar/default/{DEFAULT_AVATAR}"
def register_user(usr, pwd, avatar="default_avatar.png"):
conn = get_db_connection()
try:
cursor = conn.cursor()
# 创建用户专属头像目录
user_avatar_dir = os.path.join(AVATAR_BASE_DIR, usr)
os.makedirs(user_avatar_dir, exist_ok=True)
cursor.execute("INSERT INTO users (name, passwd, avatar) VALUES (?, ?, ?)",
(usr, pwd, avatar))
conn.commit()
return {"type": "register_1", "error": False, "message": "User registered successfully"}
except sqlite3.IntegrityError:
return {"type": "register_0", "success": False, "message": "Username already exists"}
except sqlite3.Error as e:
return {"type": "register_-1", "error": True, "message": str(e)}
finally:
conn.close()
def generate_token(username):
token = secrets.token_hex(16)
tokens[token] = {'username': username, 'timestamp': time.time()}
return token
def validate_token(token):
if token in tokens:
if time.time() - tokens[token]['timestamp'] < 3600:
tokens[token]['timestamp'] = time.time()
return tokens[token]['username']
return None
@app.route("/api/register", methods=['POST'])
def register1():
vl = request.get_json()
usr = vl.get('username')
pwd = vl.get('password')
avatar = vl.get('avatar', 'default_avatar.png')
# 验证用户名
if not validate_username(usr):
return jsonify({
"type": "register_-2",
"success": False,
"message": "Invalid username. Must be 2-20 characters, start with a letter or symbol, and contain only letters, numbers, or symbols: _!@#$%^&+-~?"
}), 400
if avatar and not (avatar.endswith('.png') or avatar.endswith('.jpg')):
return jsonify({
"type": "register_0",
"success": False,
"message": "Invalid avatar format. Only .png or .jpg allowed"
}), 400
result = register_user(usr, pwd, avatar)
if result.get('success', True): # 成功注册时返回的字典有'success'键
return jsonify(result)
else:
return jsonify(result), 403 if result['message'] == "Username already exists" else 500
@app.route("/api/login", methods=['POST'])
def login():
data = request.get_json()
username = data['username']
# 检查账号是否已登录
if username in active_connections:
# 检查连接是否仍然活跃
conn_info = active_connections[username]
try:
# 发送测试消息检查连接是否有效
conn_info['conn'].sendall(json.dumps({"type": "ping"}).encode('utf-8'))
logger.info(f"用户 {username} 的连接仍然活跃")
return jsonify({
"type": "login",
"status": "error_0",
"message": "Account already logged in"
}), 409
except:
# 连接已失效,清理旧连接
logger.warning(f"清理无效连接: {username}")
if username in active_connections:
del active_connections[username]
if conn_info['conn'] in chat_connections:
chat_connections.remove(conn_info['conn'])
if isuserxist(username) and ispsswdright(username, data['password']):
token = generate_token(username)
avatar = get_avatar(username)
return jsonify({
"type": "login_1",
"status": "success",
"token": token,
"avatar": avatar
})
return jsonify({
"type": "login_0",
"status": "error",
"message": "Invalid credentials"
}), 401
# 添加头像静态路由
@app.route("/avatar/<username>/<filename>", methods=['GET'])
def serve_avatar(username, filename):
try:
avatar_dir = os.path.join(AVATAR_BASE_DIR, username)
return send_from_directory(avatar_dir, filename)
except FileNotFoundError:
# 如果找不到头像,返回默认头像
return send_from_directory(AVATAR_BASE_DIR, DEFAULT_AVATAR)
@app.route("/api/chat", methods=['POST'])
def chat():
token = request.headers.get('Authorization')
username = validate_token(token)
if not username:
return jsonify({"type": "chat", "status": "error"}), 401
data = request.get_json()
message = {
"type": "chat",
"user": username,
"message": data['message'],
"avatar": get_avatar(username)
}
broadcast_message(message)
return jsonify({"type": "chat", "status": "success"})
def broadcast_message(message, sender=None):
for conn in chat_connections[:]: # 使用副本迭代
try:
conn.sendall(json.dumps(message).encode('utf-8'))
except:
# 连接异常时移除
for uname, info in list(active_connections.items()):
if info['conn'] == conn:
logger.warning(f"广播时移除无效连接: {uname}")
del active_connections[uname]
break
if conn in chat_connections:
chat_connections.remove(conn)
def handle_socket_message(data, addr, conn):
try:
action = data.get('type')
if action == 'register':
username = data.get('username')
password = data.get('password')
avatar = data.get('avatar', 'default_avatar.png')
# 验证用户名
if not validate_username(username):
response = {
"type": "register_-2",
"success": False,
"message": "Invalid username format"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
if avatar and not (avatar.endswith('.png') or avatar.endswith('.jpg')):
response = {
"type": "register_-3",
"success": False,
"message": "Invalid avatar format. Only .png or .jpg allowed"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
result = register_user(username, password, avatar)
conn.sendall(json.dumps(result).encode('utf-8'))
return result
elif action == 'login':
username = data['username']
password = data['password']
# 检查账号是否已登录且连接有效
if username in active_connections:
conn_info = active_connections[username]
try:
# 测试连接是否仍然有效
conn_info['conn'].sendall(json.dumps({"type": "ping"}).encode('utf-8'))
logger.info(f"用户 {username} 尝试登录但已有活跃连接")
response = {
"type": "login",
"status": "error_-1",
"message": "Account already logged in"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
except:
# 连接已失效,清理旧连接
logger.warning(f"清理无效连接后允许登录: {username}")
if username in active_connections:
del active_connections[username]
if conn_info['conn'] in chat_connections:
chat_connections.remove(conn_info['conn'])
if isuserxist(username) and ispsswdright(username, password):
# 添加新连接
active_connections[username] = {'conn': conn, 'ip': addr[0], 'last_active': time.time()}
if conn not in chat_connections:
chat_connections.append(conn)
token = generate_token(username)
avatar = get_avatar(username)
response = {
"type": "login",
"status": "success",
"message": "Login successful",
"token": token,
"username": username,
"avatar": avatar
}
conn.sendall(json.dumps(response).encode('utf-8'))
logger.info(f"用户 {username} 登录成功")
return response
else:
response = {
"type": "login",
"status": "error_0",
"message": "Invalid credentials"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
elif action == 'chat':
token = data.get('token')
if not token:
response = {
"type": "chat",
"status": "error_Mt",
"message": "Missing token"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
username = validate_token(token)
if not username:
response = {
"type": "chat",
"status": "error_It",
"message": "Invalid token"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
if username not in active_connections:
response = {
"type": "chat",
"status": "error_Nli",
"message": "Not logged in"
}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
# 更新最后活跃时间
active_connections[username]['last_active'] = time.time()
message = {
"type": "chat",
"user": username,
"message": data['message'],
"avatar": get_avatar(username)
}
broadcast_message(message)
response = {"type": "chat", "status": "success"}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
elif action == 'heartbeat':
# 心跳检测
token = data.get('token')
if token:
username = validate_token(token)
if username and username in active_connections:
# 更新最后活跃时间
active_connections[username]['last_active'] = time.time()
response = {"type": "heartbeat", "status": "success"}
conn.sendall(json.dumps(response).encode('utf-8'))
return response
return {"type": "heartbeat", "status": "error"}
except Exception as e:
logger.error(f"处理消息时出错: {str(e)}")
response = {
"status": "error",
"message": str(e)
}
try:
conn.sendall(json.dumps(response).encode('utf-8'))
except:
pass
return response
def check_inactive_connections():
"""定期检查不活跃的连接并清理"""
while True:
time.sleep(60) # 每分钟检查一次
current_time = time.time()
inactive_users = []
for username, info in list(active_connections.items()):
# 5分钟无活动视为不活跃
if current_time - info['last_active'] > 300:
logger.warning(f"检测到不活跃用户: {username}, 最后活跃: {current_time - info['last_active']}秒前")
inactive_users.append(username)
for username in inactive_users:
info = active_connections[username]
try:
info['conn'].close()
except:
pass
if username in active_connections:
del active_connections[username]
if info['conn'] in chat_connections:
chat_connections.remove(info['conn'])
logger.info(f"已清理不活跃用户: {username}")
def run_socket_server():
socket_server.bind(("0.0.0.0", 8889))
socket_server.listen()
logger.info("Socket server running on port 8889")
# 启动连接检查线程
threading.Thread(target=check_inactive_connections, daemon=True).start()
while True:
conn, addr = socket_server.accept()
logger.info(f"Socket client connected: {addr}")
try:
while True:
data = conn.recv(1024)
if not data:
break
try:
decoded_data = data.decode('utf-8', errors='ignore')
json_data = json.loads(decoded_data)
response = handle_socket_message(json_data, addr, conn)
except json.JSONDecodeError:
response = {
"type": "error",
"status": "error",
"message": "Invalid JSON"
}
conn.sendall(json.dumps(response).encode('utf-8'))
except Exception as e:
logger.error(f"处理数据时出错: {str(e)}")
response = {
"type": "error",
"status": "error",
"message": str(e)
}
conn.sendall(json.dumps(response).encode('utf-8'))
except (ConnectionResetError, BrokenPipeError):
logger.warning(f"Client {addr} disconnected abruptly")
finally:
# 清理断开的连接
for username, info in list(active_connections.items()):
if info['conn'] == conn:
del active_connections[username]
logger.info(f"用户 {username} 断开连接")
break
if conn in chat_connections:
chat_connections.remove(conn)
try:
conn.close()
except:
pass
logger.info(f"Connection closed for {addr}")
if __name__ == '__main__':
with get_db_connection() as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS users
(name TEXT PRIMARY KEY,
passwd TEXT,
avatar TEXT DEFAULT 'default_avatar.png')''')
# 确保头像目录存在
os.makedirs(AVATAR_BASE_DIR, exist_ok=True)
threading.Thread(target=run_socket_server, daemon=True).start()
app.run(port=5001, host='0.0.0.0')

187
ChatServer/CS3.1.py Normal file
View File

@ -0,0 +1,187 @@
import threading
import json
from flask import Flask, jsonify, request
import sqlite3
import socket
import base64
import secrets
import time
app = Flask(__name__)
socket_server = socket.socket()
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
active_users = {}
chat_connections = []
tokens = {}
def get_db_connection():
conn = sqlite3.connect("usr.db")
conn.row_factory = sqlite3.Row
return conn
def isuserxist(name):
cn = get_db_connection()
csr = cn.cursor()
csr.execute('SELECT * FROM users WHERE name = ?', (name,))
rst = csr.fetchone()
cn.close()
if rst is not None:
return True
else:
return False
def ispsswdright(name,passwd):
cn = get_db_connection()
csr = cn.cursor()
csr.execute("SELECT COUNT(*) FROM users WHERE name=?", (name,))
row_count = csr.fetchone()[0]
password = None
if row_count > 0:
csr.execute("SELECT passwd FROM users WHERE name=?", (name,))
password = csr.fetchone()[0]
if password == passwd:
return True
else:
return False
def register_user(usr, pwd):
conn = get_db_connection()
csr2 = conn.cursor()
csr2.execute('SELECT * FROM users WHERE name = ?', (usr,))
result = csr2.fetchone()
if result is not None:
return {"type": "register_0", "success": False, "message": "Username already exists"}
else:
try:
cursor = conn.cursor()
cursor.execute("INSERT INTO users (name, passwd) VALUES (?, ?)", (usr, pwd))
conn.commit()
return {"type": "register_1", "success": True, "message": "User registered successfully"}
except sqlite3.Error as e:
return {"type": "register_0", "success": False, "message": str(e)}
finally:
conn.close()
def generate_token(username):
token = secrets.token_hex(16)
tokens[token] = {'username': username, 'timestamp': time.time()}
return token
def validate_token(token):
if token in tokens:
if time.time() - tokens[token]['timestamp'] < 3600:
tokens[token]['timestamp'] = time.time()
return tokens[token]['username']
return None
@app.route("/api/register", methods=['POST'])
def register1():
vl = request.get_json()
usr = vl.get('username')
pwd = vl.get('password')
result = register_user(usr, pwd)
if result['success']:
return jsonify(result)
else:
return jsonify(result), 403 if result['message'] == "Username already exists" else 500
@app.route("/api/login", methods=['POST'])
def login():
data = request.get_json()
if isuserxist(data['username']):
if ispsswdright(data['username'], data['password']):
token = generate_token(data['username'])
return jsonify({"type": "login_1", "status": "success", "token": token})
return jsonify({"type": "login_0", "status": "error"}), 401
@app.route("/api/chat", methods=['POST'])
def chat():
token = request.headers.get('Authorization')
username = validate_token(token)
if not username:
return jsonify({"type": "chat", "status": "error"}), 401
data = request.get_json()
message = {
"type": "chat",
"user": username,
"message": data['message']
}
broadcast_message(message)
return jsonify({"type": "chat", "status": "success"})
def broadcast_message(message, sender=None):
for conn in chat_connections:
try:
conn.sendall(json.dumps(message).encode())
except:
chat_connections.remove(conn)
def handle_socket_message(data, addr, conn):
try:
action = data.get('type')
if action == 'register':
result = register_user(data.get('username'), data.get('password'))
if result['success']:
return {"type": "register_1","status": "success", "message": result['message']}
else:
return {"type": "register_0","status": "error", "message": result['message']}
elif action == 'login':
if isuserxist(data['username']):
if ispsswdright(data['username'], data['password']):
active_users[addr[0]] = data['username']
tk = base64.b64encode(data['username'].encode('utf-8'))
chat_connections.append(conn)
return {"type": "login_1", "status": "success", "message": "Login successful", "token": generate_token(data['username']), "username": data['username']}
return {"type": "login_0", "status": "error", "message": "Invalid credentials", "username": data['username']}
elif action == 'chat':
if addr[0] in active_users:
message = {
"type": "chat",
"user": active_users[addr[0]],
"message": data['message']
}
broadcast_message(message)
return {"type": "chat", "status": "success"}
return {"type": "chat", "status": "error", "message": "Not logged in"}
except Exception as e:
return {"status": "error", "message": str(e)}
def run_socket_server():
socket_server.bind(("localhost", 8889))
socket_server.listen()
print("Socket server running on port 8889")
while True:
conn, addr = socket_server.accept()
print(f"Socket client connected: {addr}")
try:
while True:
data = conn.recv(1024)
if not data:
if addr[0] in active_users:
del active_users[addr[0]]
break
try:
json_data = json.loads(data.decode())
response = handle_socket_message(json_data, addr, conn)
conn.sendall(json.dumps(response).encode())
except json.JSONDecodeError:
conn.sendall(json.dumps(
{"type": "register_0", "status": "error", "message": "Invalid JSON"}
).encode())
except ConnectionResetError:
if addr[0] in active_users:
del active_users[addr[0]]
print(f"Client {addr} disconnected")
finally:
if conn in chat_connections:
chat_connections.remove(conn)
conn.close()
if __name__ == '__main__':
with get_db_connection() as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS users
(name TEXT, passwd TEXT)''')
threading.Thread(target=run_socket_server).start()
app.run(port=5001)