Qt+Qml客户端和Python服务端的网络通信原型

Qt+Qml客户端和Python服务端的网络通信原型

一、背景介绍

1、什么是网络通信原型?

这个原型演示了现代客户端-服务器架构的基本实现。

2、为什么用Qt+Qml和Python组合?

  • Qt+Qml :擅长构建美观的图形界面,适合需要丰富用户交互的场景
  • Python :在数据处理后端服务方面有强大生态,开发效率高
  • 优势互补:Python处理复杂业务逻辑,Qt展示精美界面,各取所长

3、适用场景

  • 物联网设备监控面板
  • 数据可视化控制系统
  • 远程设备管理工具
  • 学习网络编程和GUI开发的入门项目

二、功能

  • 自动状态更新:服务端每秒发送状态信息
  • 多种预设命令:获取信息、计算、回显等
  • 自定义命令:支持发送任意JSON格式命令
  • 实时响应显示
  • 连接状态监控

三、效果图

四、扩展自定义命令

1、编写处理函数

python 复制代码
def control_light_handler(data):
    """控制灯光命令处理函数"""
    light_status = data.get("status", "off")
    
    if light_status == "on":
        # 实际项目中这里会控制真实的硬件
        result = "灯光已打开"
    elif light_status == "off":
        result = "灯光已关闭"
    else:
        return {"status": "error", "message": "未知的灯光状态"}
    
    return {"status": "success", "data": {"result": result}}

2、注册到服务器

python 复制代码
# 告诉服务器:当收到"control_light"命令时,调用control_light_handler函数
server.register_command_handler("control_light", control_light_handler)

3、在客户端发送命令

qml 复制代码
// 在QML中添加一个按钮
Button {
    text: "打开灯光"
    onClicked: {
        tcpClient.sendCommand("control_light", {"status": "on"})
    }
}

五、QML数据绑定解析

数据绑定是实现动态UI更新的核心机制,以下是:

1、数据绑定流程

复制代码
TCP信号 → 属性更新 → UI自动重绘
    ↓
onStatusUpdateReceived(status) → serverStatus = status
    ↓
StatusItem.value/color自动更新 → 界面刷新

2、绑定优势

2.1、 声明式编程 - 说什么,而不是怎么做
qml 复制代码
// 传统方式(命令式):需要手动更新
function updateStatus(newStatus) {
    statusLabel.text = newStatus.text
    statusLabel.color = newStatus.color
    progressBar.value = newStatus.value
    // ... 很多更新代码
}

// QML方式(声明式):只需声明关系
Label {
    text: serverStatus.text
    color: serverStatus.color
}
ProgressBar {
    value: serverStatus.value
}
// 当serverStatus变化时,一切都自动更新!
2.2、 减少胶水代码 - 更少的bug,更快的开发

传统方式可能需要大量这样的代码:

javascript 复制代码
// 繁琐的更新代码
socket.on('data', function(data) {
    updateStatusDisplay(data.status);
    updateButtonState(data.connected);
    updateErrorDisplay(data.error);
    // ... 更多更新函数
});

QML中只需要:

qml 复制代码
Connections {
    target: tcpClient
    function onStatusUpdateReceived(status) {
        serverStatus = status  // 一行代码搞定!
    }
}
2.3、 自动同步 - 数据一致性保证

由于所有UI都绑定到数据源,不会出现数据显示不一致的情况:

  • 不会出现A界面显示"已连接",B按钮却显示"连接"的bug
  • 数据变化时,所有相关界面同时更新

这种数据绑定机制使得UI能够实时响应后端数据变化,是QML框架的核心特性之一。

3、主要的数据绑定功能

3.1、 属性绑定
qml 复制代码
// 直接属性绑定
text: tcpClient.connected ? "断开" : "连接"
color: tcpClient.connected ? "green" : "red"
  • 按钮文本和状态标签颜色自动响应 tcpClient.connected状态变化
  • 无需手动更新,QML引擎自动处理依赖关系
3.2、 状态显示绑定
qml 复制代码
StatusItem {
    label: "CPU利用率"
    value: serverStatus.cpu_usage ? (serverStatus.cpu_usage * 100).toFixed(1) + "%" : "N/A"
    color: serverStatus.cpu_usage > 0.8 ? "red" : serverStatus.cpu_usage > 0.6 ? "orange" : "green"
}
  • valuecolor属性绑定到serverStatus.cpu_usage
  • 当服务器状态更新时,UI自动重新计算并显示新值
3.3、 可见性绑定
qml 复制代码
Label {
    visible: tcpClient.lastError  // 仅当有错误时显示
    text: "Error: " + tcpClient.lastError
}
  • 错误标签的可见性绑定到tcpClient.lastError属性
  • 有错误时自动显示,无错误时自动隐藏
3.4、 布局尺寸绑定
qml 复制代码
ColumnLayout {
    anchors.fill: parent  // 绑定到父窗口尺寸
    anchors.margins: 10
}
  • 布局自动填充整个父窗口
  • 窗口大小改变时,布局自动调整
3.5、 模型数据绑定
qml 复制代码
Repeater {
    model: [{"text": "获取服务器信息", "command": "get_info", ...}]
    Button {
        text: modelData.text  // 绑定到模型数据
        onClicked: tcpClient.sendCommand(modelData.command, modelData.data)
    }
}
  • 按钮文本和命令数据绑定到模型数组
  • 模型变化时,UI自动更新
3.6、 信号-槽绑定
xml 复制代码
Connections {
    target: tcpClient
    function onStatusUpdateReceived(status) {
        serverStatus = status  // 更新绑定源数据
    }
}
  • 当TCP客户端发出statusUpdateReceived信号时自动调用
  • 更新serverStatus,触发所有相关UI更新

六、代码实现

整体架构图

复制代码
┌─────────────────┐    JSON over TCP    ┌─────────────────┐
│   QML客户端      │ ←────────────────→  │   Python服务端   │
│                 │                     │                 │
│ • 用户界面       │                     │ • 业务逻辑处理   │
│ • 数据绑定       │                     │ • 命令路由       │
│ • 网络通信       │                     │ • 状态广播       │
└─────────────────┘                     └─────────────────┘

1、 Python服务端 - 智能命令处理器

1.1、消息协议设计
python 复制代码
# 数据包格式:类型(1字节) + 长度(4字节) + 数据(JSON)
# 1: 状态更新, 2: 客户端命令, 3: 命令响应

def _pack_message(self, message_type, data):
    """打包消息:确保数据完整传输"""
    data_bytes = json.dumps(data).encode('utf-8')
    data_length = len(data_bytes)
    
    # 使用struct确保二进制数据的正确格式
    header = struct.pack('<BI', message_type, data_length)
    return header + data_bytes
1.2、命令路由机制
python 复制代码
class CommandServer:
    def __init__(self):
        # 命令处理器字典:命令名 → 处理函数
        self.command_handlers = {
            "get_info": self._handle_get_info,      # 获取信息
            "calculate": self._handle_calculate,    # 数学计算
            "echo": self._handle_echo,             # 回显测试
            # 可以轻松扩展新命令!
        }
    
    def process_command(self, command_data):
        """智能命令路由"""
        command = command_data["command"]
        
        if command in self.command_handlers:
            # 找到对应的处理器并执行
            handler = self.command_handlers[command]
            return handler(command_data.get("data", {}))
        else:
            return {"status": "error", "message": f"未知命令: {command}"}
1.3、完整代码
python 复制代码
import socket
import json
import time
import threading
from datetime import datetime
import logging
import struct
import signal
import sys

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class CommandServer:
    def __init__(self, host='0.0.0.0', port=8000):
        self.host = host
        self.port = port
        self.socket = None
        self.client_conn = None
        self.client_addr = None
        self.running = False
        self.shutting_down = False
        self.server_status = {
            "cpu_usage": 0.0,
            "memory_usage": 0.0,
            "connected_clients": 0,
            "uptime": 0,
            "timestamp": ""
        }
        
        # 命令处理回调函数字典
        self.command_handlers = {
            "get_info": self._handle_get_info,
            "calculate": self._handle_calculate,
            "set_interval": self._handle_set_interval,
            "shutdown": self._handle_shutdown,
            "echo": self._handle_echo
        }
        
        self.start_time = time.time()
        self.max_packet_size = 1024 * 1024  # 1MB最大包大小
        self.header_size = struct.calcsize('<BI')  # 计算头部大小
        
        # 设置信号处理
        self._setup_signal_handlers()
    
    def _setup_signal_handlers(self):
        """设置信号处理器"""
        def signal_handler(sig, frame):
            logger.info(f"Received signal {sig}, initiating graceful shutdown...")
            self.graceful_shutdown()
        
        signal.signal(signal.SIGINT, signal_handler)   # Ctrl+C
        signal.signal(signal.SIGTERM, signal_handler)  # 终止信号
    
    def graceful_shutdown(self):
        """优雅关闭服务器"""
        if self.shutting_down:
            return
            
        self.shutting_down = True
        logger.info("Initiating graceful shutdown...")
        
        # 设置运行标志为False
        self.running = False
        
        # 如果有客户端连接,先关闭客户端连接
        if self.client_conn:
            try:
                logger.info("Closing client connection...")
                self.client_conn.close()
            except Exception as e:
                logger.error(f"Error closing client connection: {e}")
            finally:
                self.client_conn = None
        
        # 关闭服务器socket
        if self.socket:
            try:
                logger.info("Closing server socket...")
                self.socket.close()
            except Exception as e:
                logger.error(f"Error closing server socket: {e}")
            finally:
                self.socket = None
        
        logger.info("Server shutdown complete")
    
    def _pack_message(self, message_type, data):
        """打包消息:类型(1字节) + 数据长度(4字节) + 数据"""
        try:
            if isinstance(data, dict):
                data_str = json.dumps(data, ensure_ascii=False)
            else:
                data_str = str(data)
            
            data_bytes = data_str.encode('utf-8')
            data_length = len(data_bytes)
            
            # 使用小端字节序打包
            header = struct.pack('<BI', message_type, data_length)
            packet = header + data_bytes
            return packet
        except Exception as e:
            logger.error(f"Pack message error: {e}")
            return None
    
    def _unpack_message(self, data):
        """解包消息"""
        try:
            # 检查是否有足够的数据读取头部
            if len(data) < self.header_size:
                return None, None, data  # 数据不完整,等待更多数据
            
            # 解包头部
            header = data[:self.header_size]
            message_type, data_length = struct.unpack('<BI', header)
            
            # 检查数据包总大小
            total_packet_size = self.header_size + data_length
            if len(data) < total_packet_size:
                return None, None, data  # 数据不完整
            
            # 检查数据长度是否合理
            if data_length > self.max_packet_size:
                logger.warning(f"Packet too large: {data_length} bytes")
                # 跳过这个过大的数据包
                remaining_data = data[total_packet_size:]
                return None, remaining_data, None
            
            # 提取数据部分
            data_bytes = data[self.header_size:total_packet_size]
            remaining_data = data[total_packet_size:]
            
            # 解析JSON数据
            try:
                data_str = data_bytes.decode('utf-8')
                data_obj = json.loads(data_str)
                return message_type, data_obj, remaining_data
            except (json.JSONDecodeError, UnicodeDecodeError) as e:
                logger.error(f"Data decode error: {e}")
                return None, remaining_data, None
                
        except struct.error as e:
            logger.error(f"Unpack error: {e}, data length: {len(data)}")
            # 如果解包失败,清空缓冲区
            return None, None, b''
        except Exception as e:
            logger.error(f"Unexpected unpack error: {e}")
            return None, None, b''
    
    def _send_message(self, conn, message_type, data):
        """发送消息"""
        try:
            packet = self._pack_message(message_type, data)
            if packet is None:
                logger.error("Failed to pack message")
                return False
                
            total_sent = 0
            while total_sent < len(packet):
                sent = conn.send(packet[total_sent:])
                if sent == 0:
                    raise RuntimeError("Socket connection broken")
                total_sent += sent
            return True
        except Exception as e:
            logger.error(f"Send message error: {e}")
            return False
    
    def _validate_command(self, command_data):
        """验证命令数据的合法性"""
        if not isinstance(command_data, dict):
            return False, "Command data must be a dictionary"
        
        if 'command' not in command_data:
            return False, "Missing 'command' field"
        
        command = command_data['command']
        if not isinstance(command, str):
            return False, "Command must be a string"
        
        if len(command) > 100:  # 命令名长度限制
            return False, "Command name too long"
        
        if not command.isidentifier():  # 检查命令名是否合法
            return False, "Command name must be a valid identifier"
        
        # 检查数据字段
        if 'data' in command_data and not isinstance(command_data['data'], dict):
            return False, "Data field must be a dictionary"
        
        return True, "Valid"
    
    def _handle_get_info(self, data):
        """处理获取信息命令"""
        return {
            "status": "success",
            "data": {
                "server_name": "CommandServer v1.0",
                "version": "1.0.0",
                "start_time": datetime.fromtimestamp(self.start_time).isoformat(),
                "current_time": datetime.now().isoformat(),
                "max_packet_size": self.max_packet_size,
                "header_size": self.header_size,
                "server_status": "shutting_down" if self.shutting_down else "running"
            }
        }
    
    def _handle_calculate(self, data):
        """处理计算命令"""
        try:
            operation = data.get("operation")
            a = data.get("a", 0)
            b = data.get("b", 0)
            
            # 输入验证
            if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
                return {"status": "error", "message": "Parameters must be numbers"}
            
            result = 0
            if operation == "add":
                result = a + b
            elif operation == "subtract":
                result = a - b
            elif operation == "multiply":
                result = a * b
            elif operation == "divide":
                if b == 0:
                    return {"status": "error", "message": "Division by zero"}
                result = a / b
            else:
                return {"status": "error", "message": f"Unknown operation: {operation}"}
            
            return {
                "status": "success",
                "data": {
                    "operation": operation,
                    "result": result,
                    "expression": f"{a} {operation} {b} = {result}"
                }
            }
        except Exception as e:
            return {"status": "error", "message": f"Calculation error: {str(e)}"}
    
    def _handle_set_interval(self, data):
        """处理设置间隔命令"""
        interval = data.get("interval", 1.0)
        if not isinstance(interval, (int, float)):
            return {"status": "error", "message": "Interval must be a number"}
        if interval < 0.1 or interval > 60:
            return {"status": "error", "message": "Interval must be between 0.1 and 60 seconds"}
        return {
            "status": "success", 
            "data": {"new_interval": interval},
            "message": f"Interval updated to {interval} seconds"
        }
    
    def _handle_shutdown(self, data):
        """处理关闭命令"""
        if self.shutting_down:
            return {
                "status": "error",
                "message": "Server is already shutting down"
            }
            
        delay = data.get("delay", 5)
        if not isinstance(delay, int) or delay < 0 or delay > 3600:
            return {"status": "error", "message": "Delay must be integer between 0 and 3600"}
        
        # 启动延迟关闭
        def delayed_shutdown():
            time.sleep(delay)
            self.graceful_shutdown()
        
        shutdown_thread = threading.Thread(target=delayed_shutdown)
        shutdown_thread.daemon = True
        shutdown_thread.start()
        
        return {
            "status": "success",
            "data": {"shutdown_in": delay},
            "message": f"Server will shutdown in {delay} seconds"
        }
    
    def _handle_echo(self, data):
        """处理回显命令"""
        message = data.get("message", "")
        if not isinstance(message, str):
            return {"status": "error", "message": "Message must be a string"}
        if len(message) > 1000:
            return {"status": "error", "message": "Message too long"}
        return {
            "status": "success",
            "data": {"echo": message},
            "message": f"Echo: {message}"
        }
    
    def process_command(self, command_data):
        """处理客户端命令"""
        try:
            # 验证命令数据
            is_valid, validation_msg = self._validate_command(command_data)
            if not is_valid:
                return {
                    "status": "error",
                    "message": f"Invalid command format: {validation_msg}"
                }
            
            command = command_data["command"]
            data = command_data.get("data", {})
            
            if command in self.command_handlers:
                logger.info(f"Processing command: {command}")
                response = self.command_handlers[command](data)
                response["command"] = command
                return response
            else:
                return {
                    "status": "error",
                    "message": f"Unknown command: {command}",
                    "command": command
                }
        except Exception as e:
            logger.error(f"Error processing command: {e}")
            return {
                "status": "error",
                "message": f"Command processing error: {str(e)}",
                "command": command_data.get("command", "unknown")
            }
    
    def update_server_status(self):
        """更新服务器状态信息"""
        self.server_status["uptime"] = round(time.time() - self.start_time, 2)
        self.server_status["timestamp"] = datetime.now().isoformat()
        # 模拟CPU和内存使用率
        self.server_status["cpu_usage"] = round((time.time() % 100) / 100, 2)
        self.server_status["memory_usage"] = round(30 + (time.time() % 70), 2)
        self.server_status["connected_clients"] = 1 if self.client_conn else 0
        self.server_status["server_status"] = "shutting_down" if self.shutting_down else "running"
    
    def status_broadcast_worker(self):
        """状态广播工作线程"""
        while self.running and not self.shutting_down:
            if self.client_conn:
                try:
                    self.update_server_status()
                    self._send_message(self.client_conn, 1, {  # 类型1: 状态更新
                        "type": "status_update",
                        "data": self.server_status
                    })
                except Exception as e:
                    logger.error(f"Error sending status update: {e}")
                    break
            time.sleep(1)  # 每秒发送一次
    
    def handle_client(self):
        """处理客户端连接"""
        logger.info(f"Client connected: {self.client_addr}")
        
        # 为每个客户端连接维护接收缓冲区
        receive_buffer = b''
        
        # 启动状态广播线程
        status_thread = threading.Thread(target=self.status_broadcast_worker)
        status_thread.daemon = True
        status_thread.start()
        
        try:
            while self.running and not self.shutting_down:
                # 设置socket超时,以便定期检查关闭状态
                self.client_conn.settimeout(1.0)
                
                try:
                    # 接收数据
                    data = self.client_conn.recv(4096)
                    if not data:
                        logger.info("Client disconnected (no data)")
                        break
                    
                    receive_buffer += data
                    logger.debug(f"Received {len(data)} bytes, buffer size: {len(receive_buffer)}")
                    
                    # 处理缓冲区中的所有完整数据包
                    processed_count = 0
                    while True:
                        message_type, message_data, remaining_data = self._unpack_message(receive_buffer)
                        
                        if message_type is None:
                            # 没有完整的数据包,等待更多数据
                            if processed_count == 0 and len(receive_buffer) > self.max_packet_size:
                                # 缓冲区过大但没有完整包,可能是协议错误,清空缓冲区
                                logger.warning(f"Buffer too large ({len(receive_buffer)} bytes), clearing buffer")
                                receive_buffer = b''
                            break
                        
                        # 更新缓冲区为剩余数据
                        receive_buffer = remaining_data
                        processed_count += 1
                        
                        if message_type == 2:  # 类型2: 客户端命令
                            if message_data:
                                response = self.process_command(message_data)
                                self._send_message(self.client_conn, 3, response)  # 类型3: 命令响应
                            else:
                                logger.warning("Received empty message data for command")
                        else:
                            logger.warning(f"Unknown message type: {message_type}")
                            error_response = {
                                "status": "error",
                                "message": f"Unknown message type: {message_type}"
                            }
                            self._send_message(self.client_conn, 3, error_response)
                
                except socket.timeout:
                    # 超时,继续检查运行状态
                    continue
                except BlockingIOError:
                    # 非阻塞模式下无数据可用,继续检查
                    continue
        
        except ConnectionResetError:
            logger.info("Client connection reset")
        except Exception as e:
            if not self.shutting_down:  # 只有在非关闭状态下才记录错误
                logger.error(f"Client handling error: {e}")
        finally:
            logger.info(f"Client disconnected: {self.client_addr}")
            if self.client_conn:
                try:
                    self.client_conn.close()
                except Exception as e:
                    logger.error(f"Error closing client connection: {e}")
                finally:
                    self.client_conn = None
    
    def start(self):
        """启动服务器"""
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        try:
            self.socket.bind((self.host, self.port))
            self.socket.listen(1)
            self.running = True
            self.shutting_down = False
            logger.info(f"Server started on {self.host}:{self.port}")
            logger.info(f"Header size: {self.header_size} bytes")
            logger.info(f"Max packet size: {self.max_packet_size} bytes")
            logger.info("Press Ctrl+C to gracefully shutdown the server")
            
            while self.running and not self.shutting_down:
                try:
                    # 设置accept超时,以便定期检查关闭状态
                    self.socket.settimeout(1.0)
                    self.client_conn, self.client_addr = self.socket.accept()
                    # 设置socket选项
                    self.client_conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                    self.handle_client()
                except socket.timeout:
                    # 超时,继续检查运行状态
                    continue
                except OSError as e:
                    if self.shutting_down:
                        # 在关闭过程中,socket被关闭是预期的
                        break
                    else:
                        logger.error(f"Socket error: {e}")
                        break
                
        except Exception as e:
            if not self.shutting_down:  # 只有在非关闭状态下才记录错误
                logger.error(f"Server error: {e}")
        finally:
            self.graceful_shutdown()
    
    def stop(self):
        """停止服务器 - 向后兼容"""
        self.graceful_shutdown()
    
    def register_command_handler(self, command, handler):
        """注册新的命令处理函数"""
        self.command_handlers[command] = handler
        logger.info(f"Registered new command handler for: {command}")

def my_custom_handler(data):
    print(data)
    return {"status": "success", "data": {"result": "custom operation"}}

if __name__ == "__main__":
    server = CommandServer()
    server.register_command_handler("custom_command", my_custom_handler)
    
    try:
        server.start()
    except KeyboardInterrupt:
        logger.info("Server interrupted by user (KeyboardInterrupt)")
        server.graceful_shutdown()
    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        server.graceful_shutdown()

2、 Qt+Qml客户端 - 响应式用户界面

2.1、tcpclient.h
c 复制代码
#ifndef TCPCLIENT_H
#define TCPCLIENT_H

#include <QObject>
#include <QTcpSocket>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QByteArray>

class TcpClient : public QObject
{
    Q_OBJECT

    Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
    Q_PROPERTY(QString status READ status NOTIFY statusChanged)
    Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)

public:
    explicit TcpClient(QObject *parent = nullptr);
    ~TcpClient();

    bool connected() const { return m_connected; }
    QString status() const { return m_status; }
    QString lastError() const { return m_lastError; }

    Q_INVOKABLE void connectToServer(const QString &host, int port);
    Q_INVOKABLE void disconnectFromServer();
    Q_INVOKABLE void sendCommand(const QString &command, const QJsonObject &data = QJsonObject());

private:
    // 数据包类型定义
    enum MessageType {
        StatusUpdate = 1,
        ClientCommand = 2,
        CommandResponse = 3
    };

    QByteArray packMessage(quint8 type, const QJsonObject &data);
    bool unpackMessage(const QByteArray &data, quint8 &type, QJsonObject &message, QByteArray &remaining);
    void processReceivedData();

private slots:
    void onConnected();
    void onDisconnected();
    void onReadyRead();
    void onErrorOccurred(QAbstractSocket::SocketError error);

signals:
    void connectedChanged(bool connected);
    void statusChanged(const QString &status);
    void lastErrorChanged(const QString &error);
    void statusUpdateReceived(const QJsonObject &status);
    void commandResponseReceived(const QJsonObject &response);
    void serverMessageReceived(const QString &message);

private:
    QTcpSocket *m_socket;
    bool m_connected;
    QString m_status;
    QString m_lastError;
    QByteArray m_receiveBuffer;
};

#endif // TCPCLIENT_H
2.2、tcpclient.cpp
c 复制代码
#include "tcpclient.h"
#include <QDebug>
#include <QDataStream>

TcpClient::TcpClient(QObject *parent)
    : QObject(parent)
    , m_socket(new QTcpSocket(this))
    , m_connected(false)
    , m_status("Disconnected")
    , m_lastError("")
{
    connect(m_socket, &QTcpSocket::connected, this, &TcpClient::onConnected);
    connect(m_socket, &QTcpSocket::disconnected, this, &TcpClient::onDisconnected);
    connect(m_socket, &QTcpSocket::readyRead, this, &TcpClient::onReadyRead);
    connect(m_socket, &QTcpSocket::errorOccurred, this, &TcpClient::onErrorOccurred);
    // 设置socket选项
    m_socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
}

TcpClient::~TcpClient()
{
    disconnectFromServer();
}

QByteArray TcpClient::packMessage(quint8 type, const QJsonObject &data)
{
    QByteArray packet;
    QDataStream stream(&packet, QIODevice::WriteOnly);
    stream.setByteOrder(QDataStream::LittleEndian);
    // 将JSON转换为字节数组
    QJsonDocument doc(data);
    QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
    quint32 dataLength = static_cast<quint32>(jsonData.size());
    // 打包:类型(1字节) + 数据长度(4字节) + 数据
    stream << type;
    stream << dataLength;
    stream.writeRawData(jsonData.constData(), jsonData.size());
    return packet;
}

bool TcpClient::unpackMessage(const QByteArray &data, quint8 &type, 
                              QJsonObject &message, QByteArray &remaining)
{
    const int HEADER_SIZE = 5; // 类型(1) + 长度(4)
    if (data.size() < HEADER_SIZE) {
        remaining = data;
        return false;
    }

    QDataStream stream(data);
    stream.setByteOrder(QDataStream::LittleEndian);
    quint32 dataLength = 0;
    stream >> type;
    stream >> dataLength;
    // 检查数据包大小(防止过大包)
    if (dataLength > 1024 * 1024) { // 1MB限制
        qWarning() << "Packet too large:" << dataLength << "bytes";
        // 尝试跳过这个数据包
        int totalSize = HEADER_SIZE + dataLength;
        if (data.size() >= totalSize) {
            remaining = data.mid(totalSize);
        } else {
            remaining = QByteArray();
        }
        return false;
    }

    // 检查是否有完整的数据
    int totalPacketSize = HEADER_SIZE + dataLength;
    if (data.size() < totalPacketSize) {
        remaining = data;
        return false;
    }

    // 读取JSON数据
    QByteArray jsonData = data.mid(HEADER_SIZE, dataLength);

    // 解析JSON
    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);

    if (parseError.error != QJsonParseError::NoError) {
        qWarning() << "JSON parse error:" << parseError.errorString();
        // 跳过这个错误的数据包
        remaining = data.mid(totalPacketSize);
        return false;
    }

    if (!doc.isObject()) {
        qWarning() << "Received data is not a JSON object";
        remaining = data.mid(totalPacketSize);
        return false;
    }
    message = doc.object();
    remaining = data.mid(totalPacketSize);
    return true;
}

void TcpClient::processReceivedData()
{
    int processedCount = 0;
    while (!m_receiveBuffer.isEmpty()) {
        quint8 messageType;
        QJsonObject message;
        QByteArray remaining;
        if (!unpackMessage(m_receiveBuffer, messageType, message, remaining)) {
            // 没有完整的数据包,等待更多数据
            if (processedCount == 0 && m_receiveBuffer.size() > 1024 * 1024) {
                // 缓冲区过大但没有完整包,可能是协议错误,清空缓冲区
                qWarning() << "Buffer too large (" << m_receiveBuffer.size() << "bytes) clearing buffer";
                m_receiveBuffer.clear();
            }
            break;
        }

        m_receiveBuffer = remaining;
        processedCount++;

        switch (messageType) {
        case StatusUpdate:
            if (message.contains("data")) {
                QJsonObject statusData = message["data"].toObject();
                emit statusUpdateReceived(statusData);
            }
            break;
        case CommandResponse:
            emit commandResponseReceived(message);
            break;
        default:
            qWarning() << "Unknown message type:" << messageType;
            break;
        }
    }
}

void TcpClient::connectToServer(const QString &host, int port)
{
    if (m_connected) {
        disconnectFromServer();
    }
    m_status = "Connecting...";
    emit statusChanged(m_status);
    m_socket->connectToHost(host, port);
}

void TcpClient::disconnectFromServer()
{
    m_socket->disconnectFromHost();
    m_receiveBuffer.clear();
}

void TcpClient::sendCommand(const QString &command, const QJsonObject &data)
{
    if (!m_connected) {
        m_lastError = "Not connected to server";
        emit lastErrorChanged(m_lastError);
        return;
    }
    QJsonObject commandObj;
    commandObj["command"] = command;
    commandObj["data"] = data;
    QByteArray packet = packMessage(ClientCommand, commandObj);
    if (packet.isEmpty()) {
        m_lastError = "Failed to pack command";
        emit lastErrorChanged(m_lastError);
        return;
    }
    qint64 bytesWritten = m_socket->write(packet);
    if (bytesWritten == -1) {
        m_lastError = "Failed to send command: " + m_socket->errorString();
        emit lastErrorChanged(m_lastError);
    } else if (bytesWritten != packet.size()) {
        m_lastError = "Incomplete command sent: " + QString::number(bytesWritten) + "/" + QString::number(packet.size());
        emit lastErrorChanged(m_lastError);
    }
}

void TcpClient::onConnected()
{
    m_connected = true;
    m_status = "Connected";
    m_lastError = "";
    m_receiveBuffer.clear();

    emit connectedChanged(m_connected);
    emit statusChanged(m_status);
    emit lastErrorChanged(m_lastError);
}

void TcpClient::onDisconnected()
{
    m_connected = false;
    m_status = "Disconnected";
    m_receiveBuffer.clear();

    emit connectedChanged(m_connected);
    emit statusChanged(m_status);
}

void TcpClient::onReadyRead()
{
    QByteArray newData = m_socket->readAll();
    m_receiveBuffer.append(newData);
    processReceivedData();
}

void TcpClient::onErrorOccurred(QAbstractSocket::SocketError error)
{
    Q_UNUSED(error);
    m_lastError = m_socket->errorString();
    emit lastErrorChanged(m_lastError);
}
2.3、main.cpp
c 复制代码
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "tcpclient.h"
#include <QQuickStyle>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQuickStyle::setStyle("Fusion");

    // 注册TCP客户端类到QML
    qmlRegisterType<TcpClient>("com.example", 1, 0, "TcpClient");

    QQmlApplicationEngine engine;

    const QUrl url(QStringLiteral("qrc:/QtCommand/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
                         if (!obj && url == objUrl)
                             QCoreApplication::exit(-1);
                     }, Qt::QueuedConnection);

    engine.load(url);
    return app.exec();
}
2.4、main.qml
json 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import com.example 1.0

Window {
    id: window
    width: 600
    height: 900
    title: "TCP Client"
    visible: true

    TcpClient {
        id: tcpClient
    }

    // 自定义样式
    property int textFieldHeight: 40
    property int buttonHeight: 40
    property int fontSize: 14

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 10
        spacing: 10

        // 连接控制区域
        GroupBox {
            title: "连接管理"
            Layout.fillWidth: true

            RowLayout {
                width: parent.width
                spacing: 10
                TextField {
                    id: hostField
                    placeholderText: "服务器地址"
                    text: "127.0.0.1"
                    Layout.fillWidth: true
                    height: textFieldHeight
                    implicitHeight: textFieldHeight
                    font.pixelSize: fontSize
                    background: Rectangle {
                        color: "#ffffff"
                        border.color: hostField.activeFocus ? "#2196F3" : "#cccccc"
                        border.width: 1
                        radius: 4
                    }
                }
                TextField {
                    id: portField
                    placeholderText: "服务器端口"
                    text: "8000"
                    validator: IntValidator {
                        bottom: 1
                        top: 65535
                    }
                    Layout.preferredWidth: 100
                    height: textFieldHeight
                    implicitHeight: textFieldHeight
                    font.pixelSize: fontSize

                    background: Rectangle {
                        color: "#ffffff"
                        border.color: portField.activeFocus ? "#2196F3" : "#cccccc"
                        border.width: 1
                        radius: 4
                    }
                }

                Button {
                    text: tcpClient.connected ? "断开" : "连接"
                    onClicked: {
                        console.log(new Date().toLocaleTimeString())
                        if (!hostField.acceptableInput) {
                            showMessage("Error", "Invalid host address")
                            return
                        }
                        if (!portField.acceptableInput) {
                            showMessage("Error", "Invalid port number")
                            return
                        }

                        if (tcpClient.connected) {
                            tcpClient.disconnectFromServer()
                        } else {
                            tcpClient.connectToServer(hostField.text,parseInt(portField.text))
                        }
                    }
                    height: buttonHeight
                    implicitHeight: buttonHeight
                    font.pixelSize: fontSize
                    background: Rectangle {
                        color: parent.down ? "#1565C0" : parent.hovered ? "#1976D2" : "#2196F3"
                        radius: 4
                    }
                    contentItem: Text {
                        text: parent.text
                        color: "white"
                        horizontalAlignment: Text.AlignHCenter
                        verticalAlignment: Text.AlignVCenter
                        font: parent.font
                    }
                }

                Label {
                    text: tcpClient.status
                    color: tcpClient.connected ? "green" : "red"
                    font.bold: true
                    font.pixelSize: fontSize
                    Layout.minimumWidth: 120
                }
            }
        }
        // 状态显示区域
        GroupBox {
            title: "服务器状态"
            Layout.fillWidth: true
            Layout.preferredHeight: 150

            GridLayout {
                columns: 2
                width: parent.width
                rowSpacing: 5
                columnSpacing: 10
                StatusItem {
                    label: "CPU利用率"
                    value: serverStatus.cpu_usage ? (serverStatus.cpu_usage * 100).toFixed(1) + "%" : "N/A"
                    color: serverStatus.cpu_usage> 0.8 ? "red" : serverStatus.cpu_usage > 0.6 ? "orange" : "green"
                }
                StatusItem {
                    label: "内存利用率"
                    value: serverStatus.memory_usage ? serverStatus.memory_usage.toFixed(1) + " MB" : "N/A"
                    color: serverStatus.memory_usage> 80 ? "red" : serverStatus.memory_usage > 60 ? "orange" : "green"
                }
                StatusItem {
                    label: "已运行时长"
                    value: serverStatus.uptime ? formatUptime(serverStatus.uptime) : "N/A"
                }
                StatusItem {
                    label: "客户端连接数"
                    value: serverStatus.connected_clients!== undefined ? serverStatus.connected_clients : "0"
                }
                StatusItem {
                    label: "上次更新时间"
                    value: serverStatus.timestamp ? formatTimestamp(serverStatus.timestamp) : "N/A"
                }
            }
        }

        GroupBox {
            title: "命令发送区域"
            Layout.fillWidth: true
            Layout.fillHeight: true

            ColumnLayout {
                width: parent.width
                spacing: 10

                // 预设命令按钮
                GridLayout {
                    columns: 2
                    Layout.fillWidth: true
                    rowSpacing: 5
                    columnSpacing: 5

                    Repeater {
                        model: [{
                                "text": "获取服务器信息",
                                "command": "get_info",
                                "data": {}
                            }, {
                                "text": "Echo",
                                "command": "echo",
                                "data": {
                                    "message": "Hello from QML Client!"
                                }
                            }, {
                                "text": "加法",
                                "command": "calculate",
                                "data": {
                                    "operation": "add",
                                    "a": 15,
                                    "b": 25
                                }
                            }, {
                                "text": "乘法",
                                "command": "calculate",
                                "data": {
                                    "operation": "multiply",
                                    "a": 7,
                                    "b": 8
                                }
                            }]

                        Button {
                            text: modelData.text
                            onClicked: tcpClient.sendCommand(modelData.command,modelData.data)
                            Layout.fillWidth: true
                            height: buttonHeight
                            implicitHeight: buttonHeight
                            font.pixelSize: fontSize - 1
                            background: Rectangle {
                                color: parent.down ? "#388E3C" : parent.hovered ? "#4CAF50" : "#66BB6A"
                                radius: 4
                            }

                            contentItem: Text {
                                text: parent.text
                                color: "white"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                                font: parent.font
                            }
                        }
                    }
                }
                GroupBox {
                    title: "自定义命令"
                    Layout.fillWidth: true
                    ColumnLayout {
                        width: parent.width
                        spacing: 5
                        RowLayout {
                            spacing: 5
                            TextField {
                                id: customCommandField
                                Layout.fillWidth: true
                                height: textFieldHeight
                                implicitHeight: textFieldHeight
                                font.pixelSize: fontSize
                                text: "custom_command"

                                background: Rectangle {
                                    color: "#ffffff"
                                    border.color: customCommandField.activeFocus ? "#2196F3" : "#cccccc"
                                    border.width: 1
                                    radius: 4
                                }
                            }
                            Button {
                                text: "验证JSON合法性"
                                onClicked: {
                                    if (customDataField.text) {
                                        try {
                                            JSON.parse(customDataField.text)
                                            responseArea.append("✓ JSON is valid")
                                        } catch (e) {
                                            responseArea.append("✗ JSON error: " + e.toString())
                                        }
                                    } else {
                                        responseArea.append("✓ Empty data is valid")
                                    }
                                }

                                height: buttonHeight
                                implicitHeight: buttonHeight
                                font.pixelSize: fontSize - 1
                                background: Rectangle {
                                    color: parent.down ? "#F57C00" : parent.hovered ? "#FF9800" : "#FFB74D"
                                    radius: 4
                                }
                                contentItem: Text {
                                    text: parent.text
                                    color: "white"
                                    horizontalAlignment: Text.AlignHCenter
                                    verticalAlignment: Text.AlignVCenter
                                    font: parent.font
                                }
                            }
                        }

                        TextField {
                            id: customDataField
                            text: '{"key": 123}'
                            Layout.fillWidth: true

                            height: textFieldHeight
                            implicitHeight: textFieldHeight
                            font.pixelSize: fontSize

                            background: Rectangle {
                                color: "#ffffff"
                                border.color: customDataField.activeFocus ? "#2196F3" : "#cccccc"
                                border.width: 1
                                radius: 4
                            }
                        }

                        Button {
                            text: "发送命令"
                            onClicked: {
                                if (!customCommandField.acceptableInput) {
                                    showMessage("Error","Invalid command name (only letters, numbers, underscore)")
                                    return
                                }

                                if (customCommandField.text === "") {
                                    showMessage("Error","Please enter a command name")
                                    return
                                }

                                let data = {}
                                if (customDataField.text) {
                                    try {
                                        data = JSON.parse(customDataField.text)
                                    } catch (e) {
                                        showMessage("JSON Error","Invalid JSON format: " + e.toString())
                                        return
                                    }
                                }
                                tcpClient.sendCommand(customCommandField.text,data)
                            }
                            Layout.fillWidth: true

                            height: buttonHeight
                            implicitHeight: buttonHeight
                            font.pixelSize: fontSize

                            background: Rectangle {
                                color: parent.down ? "#7B1FA2" : parent.hovered ? "#9C27B0" : "#BA68C8"
                                radius: 4
                            }

                            contentItem: Text {
                                text: parent.text
                                color: "white"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                                font: parent.font
                            }
                        }
                    }
                }

                GroupBox {
                    title: "响应显示区域"
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    TextArea {
                        id: responseArea
                        placeholderText: "Server responses will appear here..."
                        readOnly: true
                        font.family: "Courier New"
                        font.pixelSize: fontSize - 1
                        selectByMouse: true
                        wrapMode: TextArea.Wrap
                        anchors.fill: parent
                        background: Rectangle {
                            color: "#f8f8f8"
                            border.color: "#cccccc"
                            border.width: 1
                            radius: 3
                        }
                    }
                }
            }
        }

        // 错误显示
        Label {
            visible: tcpClient.lastError
            text: "Error: " + tcpClient.lastError
            color: "red"
            font.bold: true
            font.pixelSize: fontSize
            Layout.fillWidth: true
        }
    }

    // 消息对话框
    MessageDialog {
        id: messageDialog
        title: "Information"
    }

    // 服务器状态数据
    property var serverStatus: ({})

    // 显示消息
    function showMessage(title, message) {
        messageDialog.title = title
        messageDialog.text = message
        messageDialog.open()
    }

    // 格式化运行时间
    function formatUptime(seconds) {
        if (!seconds)
            return "N/A"
        let hours = Math.floor(seconds / 3600)
        let minutes = Math.floor((seconds % 3600) / 60)
        let secs = Math.floor(seconds % 60)
        return hours + "h " + minutes + "m " + secs + "s"
    }

    // 格式化时间戳
    function formatTimestamp(timestamp) {
        if (!timestamp)
            return "N/A"
        let date = new Date(timestamp)
        return date.toLocaleTimeString()
    }

    // 连接信号
    Connections {
        target: tcpClient

        function onStatusUpdateReceived(status) {
            serverStatus = status
        }

        function onCommandResponseReceived(response) {
            responseArea.clear()
            let time = new Date().toLocaleTimeString()
            let output = `[${time}] === Command Response ===\n`
            output += "Command: " + (response.command || "unknown") + "\n"
            output += "Status: " + (response.status || "unknown") + "\n"
            if (response.message) {
                output += "Message: " + response.message + "\n"
            }
            if (response.data) {
                output += "Data: " + JSON.stringify(response.data,null, 2) + "\n"
            }
            output += "========================\n"
            responseArea.append(output)
        }

        function onConnectedChanged(connected) {
            responseArea.clear()
            if (connected) {
                console.log(new Date().toLocaleTimeString())
                responseArea.append("=== Connected to server ===\n")
            } else {
                responseArea.append("=== Disconnected from server ===\n")
            }
        }
    }
}
2.5、StatusItem.qml
json 复制代码
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

RowLayout {
    property string label
    property string value
    property color color: "black"
    Label {
        text: parent.label + ":"
        font.bold: true
        Layout.preferredWidth: 120
    }

    Label {
        text: parent.value
        color: parent.color
        Layout.fillWidth: true
    }
}

七、开发建议和最佳实践

1、 错误处理增强

python 复制代码
def safe_command_handler(handler):
    """装饰器:确保命令处理器不会崩溃"""
    def wrapper(data):
        try:
            return handler(data)
        except Exception as e:
            logger.error(f"Command handler error: {e}")
            return {"status": "error", "message": "Internal server error"}
    return wrapper

# 使用装饰器
@safe_command_handler
def calculate_handler(data):
    # 业务逻辑
    return result

2、 性能优化

qml 复制代码
// 对于频繁更新的数据,使用批处理
Timer {
    id: batchTimer
    interval: 16  // ~60fps
    repeat: true
    onTriggered: {
        // 批量更新UI,避免频繁重绘
        updateBatchUI()
    }
}

3、 安全性考虑

python 复制代码
def validate_command(self, command_data):
    """命令验证"""
    # 检查命令名合法性
    if not command_data['command'].isidentifier():
        return False, "Invalid command name"
    
    # 防止命令注入
    if len(command_data['command']) > 100:
        return False, "Command name too long"
    
    # 数据大小限制
    if len(str(command_data.get('data', ''))) > 10000:
        return False, "Data too large"
    
    return True, "Valid"

八、总结

这个网络通信原型展示了现代客户端-服务器应用的核心技术栈:

  • Python服务端:处理业务逻辑、命令路由、状态管理
  • Qt+Qml客户端:提供响应式、数据绑定的用户界面
  • TCP+JSON通信:可靠的数据传输和结构化数据交换
  • 可扩展架构:轻松添加新命令和功能

通过理解这个原型,你可以快速开发各种网络应用。数据绑定机制让你专注于业务逻辑,而不是繁琐的UI更新代码,大大提高开发效率。

相关推荐
chxii3 小时前
ISO 8601日期时间标准及其在JavaScript、SQLite与MySQL中的应用解析
开发语言·javascript·数据库
Teable任意门互动3 小时前
主流多维表格产品深度解析:飞书、Teable、简道云、明道云、WPS
开发语言·网络·开源·钉钉·飞书·开源软件·wps
程序员大雄学编程4 小时前
「用Python来学微积分」16. 导数问题举例
开发语言·python·数学·微积分
B站_计算机毕业设计之家4 小时前
预测算法:股票数据分析预测系统 股票预测 股价预测 Arima预测算法(时间序列预测算法) Flask 框架 大数据(源码)✅
python·算法·机器学习·数据分析·flask·股票·预测
Dreams_l5 小时前
redis中的数据类型
java·开发语言
梵得儿SHI5 小时前
Java IO 流详解:字符流(Reader/Writer)与字符编码那些事
java·开发语言·字符编码·工作原理·字符流·处理文本
yj15585 小时前
装修中怎样避坑
python
太过平凡的小蚂蚁5 小时前
Kotlin 协程中常见的异步返回与控制方式(速览)
开发语言·前端·kotlin
007php0075 小时前
京东面试题解析:同步方法、线程池、Spring、Dubbo、消息队列、Redis等
开发语言·后端·百度·面试·职场和发展·架构·1024程序员节