背景
工控机中的软件生成日志文件,想在同一局域网中的监控端电脑中监控工控机特定文件夹中的日志文件。
摘要
使用pyqt与socket搭建简单的日记监控同步系统。在初次连接服务端时,检查服务端是否包含客户端所有文件,若没有更新未包含文件。并在更新后实时监控文件夹中新增的文件。
操作方法
下载github代码,分别放入客户端与服务端电脑。若无法下载,代码将放到最后。
# 下载客户端代码
git clone https://github.com/shibo-SWD/LogFilesMonitorSystem-Client.git
# 下载服务端代码
git clone https://github.com/shibo-SWD/LogFilesMonitorSystem-Service.git
# 进入服务端根目录 并下载所需依赖库
cd LogFilesMonitorSystem-Service
pip install -r requirements.txt
# 记得在client.py更换服务端IP地址
# client = FileClient(server_host='你服务端设备ip', server_port=12345)
# 在监控端代码根目录
python ./main.py
# 在被监控客户端根目录
python ./clinet.py
监控服务端
首先在命令行运行 pip install PyQt5==5.15.11
,并按照代码架构创建。
project_root/
│
├── server/
│ ├── server_backend.py # 服务器的核心功能,如启动和处理客户端请求
│ └── server_controller.py # 控制服务器启动和停止的逻辑
│
├── ui/
│ ├── fonts.py # 字体调整
│ └── server_gui.py # GUI 界面逻辑
│
├── config/
│ └── settings.py # 配置文件,如端口、IP等设置
│
├── logs/
│ └── .log # 日志文件
│
├── utils/
│ └── helpers.py # 实用工具函数,如日志记录、文件操作等
│
├── data/
│ └── received_files/ # 存放接收到的文件
│
└── main.py # 项目入口文件
main.py
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QPushButton, QVBoxLayout, QLabel,
QTextEdit, QStatusBar, QAction, QMenuBar
)
from PyQt5.QtCore import Qt
from server.server_backend import FileServer
from ui.server_gui import ServerGUI
def main():
app = QApplication(sys.argv)
gui = ServerGUI()
gui.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
server_backend.py
import socket
import threading
import os
class FileServer:
def __init__(self, host='0.0.0.0', port=12345, save_dir='./data/received_files'):
self.host = host
self.port = port
self.save_dir = save_dir
self.clients = {}
self.client_lock = threading.Lock()
self.server_socket = None
self.is_running = False
os.makedirs(self.save_dir, exist_ok=True)
def start(self):
"""启动服务器线程以监听客户端连接"""
self.is_running = True
server_thread = threading.Thread(target=self._run_server, daemon=True)
server_thread.start()
def stop(self):
"""停止服务器"""
self.is_running = False
if self.server_socket:
self.server_socket.close()
def _run_server(self):
"""在后台运行的服务器线程"""
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print(f"Server listening on {self.host}:{self.port}")
while self.is_running:
try:
connection, client_address = self.server_socket.accept()
print(f"Accepted connection from {client_address}")
threading.Thread(target=self._handle_client, args=(connection, client_address)).start()
except OSError:
break # 当服务器 socket 关闭时会抛出 OSError 异常
self.server_socket.close()
def _handle_client(self, connection, client_address):
"""处理每个客户端的连接"""
client_ip = client_address[0]
with self.client_lock:
if client_ip not in self.clients:
self.clients[client_ip] = 0
try:
while True:
command = connection.recv(4)
if not command:
break
if command == b'LIST':
self.check_files(connection)
elif command == b'SEND':
self.receive_file(connection, client_ip)
else:
print(f"Unknown command received: {command}")
except Exception as e:
print(f"Error handling client {client_address}: {e}")
finally:
print(f"Client {client_address} disconnected.")
connection.close()
def check_files(self, connection):
"""检查客户端发送的文件列表,返回需要的文件列表"""
try:
# 接收文件列表长度
files_length_data = connection.recv(4)
if not files_length_data:
return
files_length = int.from_bytes(files_length_data, 'big')
# 接收文件列表
files_data = connection.recv(files_length).decode()
client_files = files_data.split('\n')
# 服务器已有的文件
server_files = set(os.listdir(self.save_dir))
# 计算需要发送的文件列表
files_to_request = [file for file in client_files if file not in server_files]
files_to_request_data = '\n'.join(files_to_request).encode()
# 发送需要发送的文件列表长度和数据
connection.send(len(files_to_request_data).to_bytes(4, 'big'))
connection.send(files_to_request_data)
except Exception as e:
print(f"Error checking files: {e}")
def receive_file(self, connection, client_ip):
"""接收并保存文件"""
try:
while True:
# 接收文件名长度
file_name_length_data = connection.recv(4)
if not file_name_length_data:
break
file_name_length = int.from_bytes(file_name_length_data, 'big')
# 接收文件名
file_name = connection.recv(file_name_length).decode()
file_path = os.path.join(self.save_dir, file_name)
# 接收文件大小
file_size_data = connection.recv(8)
if not file_size_data:
break
file_size = int.from_bytes(file_size_data, 'big')
received_size = 0
# 接收文件数据
with open(file_path, 'wb') as f:
while received_size < file_size:
data = connection.recv(1024)
if not data:
break
f.write(data)
received_size += len(data)
print(f"Receiving file '{file_name}': {received_size}/{file_size} bytes")
print(f"Received file '{file_name}' from {client_ip}")
with self.client_lock:
self.clients[client_ip] += 1
except Exception as e:
print(f"Error handling client {client_ip}: {e}")
server_controller.py
from server.server_backend import FileServer
class ServerController:
def __init__(self):
self.file_server = None
def start_server(self):
self.file_server = FileServer()
self.file_server.start()
def stop_server(self):
if self.file_server:
self.file_server.stop()
server_gui.py
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QPushButton, QVBoxLayout, QLabel,
QTextEdit, QStatusBar, QAction, QMenuBar, QLineEdit
)
from PyQt5.QtCore import Qt, pyqtSignal, QObject
from server.server_backend import FileServer
from ui.fonts import FontSizeDialog
import threading # 导入线程库
class Communicator(QObject):
# 定义信号
status_update = pyqtSignal(str)
log_update = pyqtSignal(str)
server_started = pyqtSignal()
server_stopped = pyqtSignal()
class ServerGUI(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
self.file_server = None
self.communicator = Communicator()
# 连接信号与槽
self.communicator.status_update.connect(self.update_status_label)
self.communicator.log_update.connect(self.update_log_text)
self.communicator.server_started.connect(self.on_server_started)
self.communicator.server_stopped.connect(self.on_server_stopped)
def initUI(self):
"""初始化UI组件"""
self.setWindowTitle('文件监控服务端')
self.resize(600, 400) # 设置窗口大小
# 中央Widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
# 状态显示
self.status_label = QLabel('服务器未启动')
layout.addWidget(self.status_label)
# 日志保存地址输入框
self.save_dir_input = QLineEdit(self)
self.save_dir_input.setText('./data/received_files')
layout.addWidget(QLabel('监控日志保存地址:'))
layout.addWidget(self.save_dir_input)
# 启动和关闭按钮
self.start_button = QPushButton('启动服务器')
self.start_button.clicked.connect(self.start_server)
layout.addWidget(self.start_button)
self.stop_button = QPushButton('关闭服务器')
self.stop_button.setEnabled(False)
self.stop_button.clicked.connect(self.stop_server)
layout.addWidget(self.stop_button)
# 日志显示区域
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
layout.addWidget(self.log_text)
central_widget.setLayout(layout)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# 菜单栏
menubar = self.menuBar()
file_menu = menubar.addMenu('文件')
about_action = QAction('关于', self)
about_action.triggered.connect(self.show_about)
file_menu.addAction(about_action)
exit_action = QAction('退出', self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 添加字体调整选项
settings_menu = menubar.addMenu('设置')
font_action = QAction('调整字体大小', self)
font_action.triggered.connect(self.open_font_size_dialog)
settings_menu.addAction(font_action)
# 主题选择菜单
theme_menu = menubar.addMenu('主题')
light_theme_action = QAction('明亮主题', self)
light_theme_action.triggered.connect(self.set_light_theme)
theme_menu.addAction(light_theme_action)
dark_theme_action = QAction('暗黑主题', self)
dark_theme_action.triggered.connect(self.set_dark_theme)
theme_menu.addAction(dark_theme_action)
def start_server(self):
"""启动服务器"""
save_dir_path = self.save_dir_input.text() # 获取输入框中的文本
# 检查输入是否为空,提供一个默认值
if not save_dir_path:
save_dir_path = './data/received_files'
self.file_server = FileServer(save_dir=save_dir_path)
# 将服务器启动放在后台线程中,以免阻塞主线程
threading.Thread(target=self._start_server_in_thread, daemon=True).start()
def _start_server_in_thread(self):
"""后台线程中启动服务器"""
self.file_server.start()
# 通过信号更新 GUI
self.communicator.status_update.emit(f'服务器正在监听 IP: {self.file_server.host} 端口: {self.file_server.port}')
self.communicator.server_started.emit()
def stop_server(self):
"""关闭服务器"""
if self.file_server:
self.file_server.stop()
self.communicator.status_update.emit('服务器已停止')
self.communicator.server_stopped.emit()
def update_status_label(self, status_text):
"""更新状态标签"""
self.status_label.setText(status_text)
def update_log_text(self, log_text):
"""更新日志文本区域"""
self.log_text.append(log_text)
def on_server_started(self):
"""服务器启动后的处理"""
self.status_bar.showMessage('服务器已启动')
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.communicator.log_update.emit('服务器已启动...')
def on_server_stopped(self):
"""服务器停止后的处理"""
self.status_bar.showMessage('服务器已停止')
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
self.communicator.log_update.emit('服务器已停止...')
def show_about(self):
"""显示关于信息"""
self.log_text.append('关于:这是一个文件监控服务端程序,使用 PyQt5 构建。')
def set_light_theme(self):
"""设置明亮主题"""
self.setStyleSheet("""
QMainWindow {
background-color: white;
color: black;
}
QLabel, QTextEdit, QPushButton {
background-color: white;
color: black;
}
""")
self.status_bar.showMessage('已切换到明亮主题')
def set_dark_theme(self):
"""设置暗黑主题"""
self.setStyleSheet("""
QMainWindow {
background-color: #2e2e2e;
color: white;
}
QLabel, QTextEdit, QPushButton {
background-color: #2e2e2e;
color: white;
}
""")
self.status_bar.showMessage('已切换到暗黑主题')
def open_font_size_dialog(self):
"""打开字体大小调整对话框"""
font_dialog = FontSizeDialog(self)
font_dialog.exec_()
def set_font_size(self, size):
"""设置整个应用的字体大小"""
self.current_font_size = size
self.setStyleSheet(f"* {{ font-size: {size}px; }}")
fonts.py
from PyQt5.QtWidgets import (
QApplication, QVBoxLayout, QLabel, QDialog, QSlider
)
from PyQt5.QtCore import Qt
class FontSizeDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("调整字体大小")
self.setGeometry(100, 100, 300, 100)
layout = QVBoxLayout()
# 添加滑块
self.slider = QSlider(Qt.Horizontal)
self.slider.setMinimum(8)
self.slider.setMaximum(24)
self.slider.setValue(12) # 默认字体大小
self.slider.setTickPosition(QSlider.TicksBelow)
self.slider.setTickInterval(2)
self.slider.valueChanged.connect(self.update_font_size)
layout.addWidget(QLabel("选择字体大小:"))
layout.addWidget(self.slider)
self.setLayout(layout)
def update_font_size(self):
"""更新主窗口的字体大小"""
font_size = self.slider.value()
self.parent().set_font_size(font_size)
setting.py
DEFAULT_HOST = '0.0.0.0'
DEFAULT_PORT = 12345
helper.py
import logging
def setup_logging():
logging.basicConfig(filename='logs/server.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def log_message(message):
logging.info(message)
被监控客户端
在要监控的电脑的文件节夹中放入下列代码。
client.py
import socket
import os
import time
from file_monitor import FileMonitor
class FileClient:
def __init__(self, server_host='127.0.0.1', server_port=12345, watch_directory='./watch_directory'):
self.server_host = server_host
self.server_port = server_port
self.watch_directory = watch_directory
def start(self):
"""启动文件监控并发送新文件"""
os.makedirs(self.watch_directory, exist_ok=True)
# 连接服务器并获取需要发送的文件列表
files_to_send = self.list_files()
# 监控文件夹并发送新文件
monitor = FileMonitor(self.watch_directory, self.send_file, files_to_send)
monitor.start()
# 保持主线程运行
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down client...")
monitor.stop()
def list_files(self):
"""列出监控目录中的所有文件,并检查哪些需要发送"""
files = os.listdir(self.watch_directory)
files_to_send = []
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
print(f"Connecting to server at {self.server_host}:{self.server_port} to list files...")
client_socket.connect((self.server_host, self.server_port))
print("Connected to server.")
# 发送指令标识 (例如 'LIST')
client_socket.send(b'LIST')
# 发送文件名列表的长度
files_data = '\n'.join(files).encode()
client_socket.send(len(files_data).to_bytes(4, 'big'))
client_socket.send(files_data)
# 接收需要发送的文件列表长度
response_length_data = client_socket.recv(4)
response_length = int.from_bytes(response_length_data, 'big')
# 接收需要发送的文件列表
response_data = client_socket.recv(response_length).decode()
files_to_send = response_data.split('\n')
except Exception as e:
print(f"Error listing files: {e}")
print(files_to_send)
return files_to_send
def send_file(self, file_path):
"""发送文件到服务端"""
with open(file_path, 'rb') as file:
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((self.server_host, self.server_port))
print(f"Connected to server at {self.server_host}:{self.server_port}")
# 发送命令
sock.sendall(b'SEND')
# 发送文件名长度和文件名
file_name_encoded = file_name.encode()
sock.sendall(len(file_name_encoded).to_bytes(4, 'big'))
sock.sendall(file_name_encoded)
# 发送文件大小
sock.sendall(file_size.to_bytes(8, 'big'))
# 发送文件内容
chunk = file.read(1024)
while chunk:
sock.sendall(chunk)
chunk = file.read(1024)
print(f"File '{file_name}' sent to server.")
if __name__ == "__main__":
client = FileClient(server_host='172.21.22.78', server_port=12345)
client.start()
file_monitor.py
import os
import time
from threading import Thread
class FileMonitor:
def __init__(self, directory, callback, initial_files_to_send=None):
self.directory = directory
self.callback = callback
self.running = False
# 使用服务器返回的需要发送的文件列表初始化
self.files_to_send = set(initial_files_to_send) if initial_files_to_send else set()
self._sync_initial_files() # 立即同步初始文件
def start(self):
"""启动文件监控线程"""
self.running = True
self.thread = Thread(target=self._monitor_directory, daemon=True)
self.thread.start()
print("File monitor started, watching directory:", self.directory)
def stop(self):
"""停止文件监控"""
self.running = False
self.thread.join()
print("File monitor stopped.")
def _sync_initial_files(self):
"""同步初始文件,将客户端有但服务端没有的文件发送给服务端"""
current_files = set(os.listdir(self.directory))
files_to_send_now = self.files_to_send
print(files_to_send_now)
for new_file in files_to_send_now:
file_path = os.path.join(self.directory, new_file)
print(f"Syncing file to server: {file_path}")
self.callback(file_path) # 使用回调函数发送文件
# self.files_to_send -= current_files
# 更新已发送的文件列表
self.files_to_send.update(files_to_send_now)
def _monitor_directory(self):
"""监控目录中的新文件"""
files_set = set(os.listdir(self.directory))
while self.running:
current_files = set(os.listdir(self.directory))
# 仅检测新文件
new_files = current_files - files_set
# 过滤掉已经在self.files_to_send中的文件,只发送新的和没有在初始列表中的文件
files_to_send_now = new_files - self.files_to_send
for new_file in files_to_send_now:
file_path = os.path.join(self.directory, new_file)
print(f"New file detected: {file_path}")
self.callback(file_path) # 调用回调函数发送文件
# 更新已存在文件列表
files_set = current_files
# 添加刚检测到的新文件到self.files_to_send中,防止重复发送
self.files_to_send.update(new_files)
time.sleep(1)
使用
在配置好后运行
# 在监控端代码根目录
python ./main.py
# 在被监控客户端根目录
python ./clinet.py