目录
1、课程设计目的
1.1 深入理解 TCP 协议的工作原理及其在网络编程中的应用
1.2 掌握 Socket 编程技术,实现客户端 - 服务器架构的网络应用
1.3 学习文件传输的基本原理和实现方法,包括文件分块传输、进度显示等
1.4 理解多线程编程在网络服务器中的应用,实现并发处理多个客户端连接
1.5 掌握网络应用程序的错误处理和异常处理机制
1.6 通过实际项目设计,提升综合运用计算机网络知识解决实际问题的能力
2、课程设计要求
2.1 功能实现要求
设计并实现基于 TCP 协议的文件传输系统,包含客户端和服务器端,支持文件上传、下载、列表查询及删除功能,确保核心功能完整可用。
2.2 代码工作量要求
代码需体现一定的复杂性和完整性,包含模块化设计、异常处理、多线程支持等,避免简单的单一功能实现。
2.3 演示与报告要求
通过代码功能演示验证系统可用性,包括界面操作、功能流程展示
编写课程设计报告,详细说明设计思路、技术实现和心得体会
3、相关知识
3.1 TCP 协议原理
面向连接的可靠传输:通过三次握手建立连接,四次挥手断开连接,保证数据有序、无丢失传输
流量控制与拥塞控制:使用滑动窗口机制控制数据发送速率,通过慢启动、拥塞避免等算法应对网络拥塞
字节流传输:数据以字节流形式传输,无消息边界,需在应用层处理数据分块
3.2 Socket 编程基础
Socket 通信模型:服务器端通过绑定端口、监听连接、接受客户端请求建立通信
TCP Socket 关键方法:bind()、listen()、accept()(服务器端),connect()(客户端)
数据传输:通过send()和recv()方法实现字节数据的发送与接收
3.3 文件操作与传输
文件分块传输:大文件拆分为固定大小数据块传输,避免内存溢出
文件元数据传输:通过 JSON 格式传输文件名、文件大小等元信息
进度计算:根据已传输字节数与总文件大小的比例计算传输进度
3.4 多线程编程
并发处理:服务器通过多线程同时处理多个客户端连接,提升系统吞吐量
线程安全:需注意共享资源(如文件目录)的访问同步,避免数据冲突
4、课程设计分析
4.1 系统架构设计
4.1.1 整体架构
C/S 架构:客户端(Client)与服务器端(Server)通过 TCP 协议建立连接
模块化设计:客户端和服务器端分别封装为独立类,各功能模块解耦(如文件传输、命令处理、界面交互)
4.1.2 数据传输流程
命令交互:使用 JSON 格式传输命令与响应(如{"command": "upload", "filename": "test.txt"})
文件传输:
上传:客户端分块发送文件数据,服务器接收并写入磁盘
下载:服务器分块读取文件数据,客户端接收并保存
删除:客户端发送删除命令,服务器验证后删除文件
4.2 服务器端实现分析
4.2.1 核心类与功能
FileTransferServer 类:
启动与监听:通过start()方法绑定端口并监听客户端连接
多线程处理:为每个客户端创建独立线程(handle_client方法),避免阻塞
命令处理:支持list(列表查询)、upload(上传)、download(下载)、delete(删除)命令
4.2.2 关键方法解析
handle_upload 方法:
接收文件元数据(文件名、大小)
分块接收文件数据,写入磁盘
错误处理:上传失败时删除不完整文件
handle_delete 方法:
验证文件存在性
执行删除操作并返回结果
4.3 客户端实现分析
4.3.1 核心类与功能
FileTransferClient 类:
连接管理:connect()和disconnect()方法管理服务器连接
文件操作:实现upload_file(上传)、download_file(下载)、delete_file(删除)功能
用户交互:通过命令行菜单提供操作界面
4.3.2 交互流程
列表查询:发送list命令获取服务器文件列表并显示
文件上传 / 下载:
显示进度条与传输速度(进度: XX% | 速度: XX KB/s)
通过字节数计算实时传输状态
文件删除:
显示文件列表供用户选择
确认后发送删除命令并反馈结果
4.4 关键技术点
TCP 可靠性保证:利用 TCP 协议的确认机制、重传机制确保文件数据完整传输
流式数据处理:通过固定缓冲区(4096 字节)分块处理大文件,避免内存压力
异常处理:服务器与客户端均包含完整的异常捕获逻辑(如文件不存在、传输中断)
并发处理:服务器使用多线程技术,支持同时处理多个客户端请求
5、相关扩展
断点续传:
拓展功能代码:在服务器和客户端代码中,我们添加了对已传输字节偏移量的记录和恢复功能。具体来说,我们使用元数据文件(文件名加上.meta后缀)来保存传输状态。在上传和下载文件时,会检查元数据文件是否存在,如果存在则恢复传输。
上传文件:客户端会发送已传输字节偏移量给服务器,服务器会根据偏移量继续接收文件。
下载文件:客户端会发送已传输字节偏移量给服务器,服务器会根据偏移量继续发送文件。
6、源代码
6.1 server
import socket
import os
import json
import threading
import time
import ssl # 拓展:SSL/TLS加密模块
class FileTransferServer:
def __init__(self, host='localhost', port=9999, buffer_size=4096, upload_dir='./server_files',
use_ssl=True, cert_file='./cert/server.crt', key_file='./cert/server.key'):
self.host = host
self.port = port
self.buffer_size = buffer_size
self.upload_dir = upload_dir
self.server_socket = None
self.use_ssl = use_ssl # 拓展:是否使用SSL
self.cert_file = cert_file # 拓展:证书文件路径
self.key_file = key_file # 拓展:私钥文件路径
# 确保上传目录存在
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
def start(self):
"""启动服务器"""
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print(f"服务器已启动,监听地址: {self.host}:{self.port}")
print("等待客户端连接...")
while True:
client_socket, client_address = self.server_socket.accept()
print(f"客户端 {client_address} 已连接")
# 为每个客户端创建一个线程
client_thread = threading.Thread(target=self.handle_client, args=(client_socket,))
client_thread.daemon = True
client_thread.start()
except Exception as e:
print(f"服务器启动失败: {e}")
finally:
if self.server_socket:
self.server_socket.close()
def handle_client(self, client_socket):
"""处理客户端请求"""
try:
while True:
# 接收客户端命令
command_data = client_socket.recv(self.buffer_size).decode('utf-8')
if not command_data:
break
command = json.loads(command_data)
cmd_type = command.get('command')
if cmd_type == 'list':
self.list_files(client_socket)
elif cmd_type == 'upload':
self.handle_upload(client_socket, command)
elif cmd_type == 'download':
self.handle_download(client_socket, command)
elif cmd_type == 'delete':
self.handle_delete(client_socket, command)
elif cmd_type == 'exit':
break
else:
response = {'status': 'error', 'message': '未知命令'}
client_socket.send(json.dumps(response).encode('utf-8'))
print("客户端已断开连接")
except Exception as e:
print(f"处理客户端请求时出错: {e}")
finally:
client_socket.close()
def list_files(self, client_socket):
"""列出服务器上的文件"""
try:
files = os.listdir(self.upload_dir)
response = {'status': 'success', 'files': files}
client_socket.send(json.dumps(response).encode('utf-8'))
except Exception as e:
response = {'status': 'error', 'message': f'获取文件列表失败: {str(e)}'}
client_socket.send(json.dumps(response).encode('utf-8'))
def handle_upload(self, client_socket, command):
"""处理文件上传"""
filename = command.get('filename')
filesize = command.get('filesize')
offset = command.get('offset', 0) # 拓展功能代码:获取已传输字节偏移量
if not filename or not filesize:
response = {'status': 'error', 'message': '缺少文件名或文件大小'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
file_path = os.path.join(self.upload_dir, filename)
metadata_path = f"{file_path}.meta" # 拓展功能代码:元数据文件路径
# 检查文件是否已存在
if os.path.exists(file_path):
if offset == 0:
response = {'status': 'error', 'message': '文件已存在'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
else:
# 恢复传输
try:
with open(metadata_path, 'r') as meta_file:
meta_data = json.load(meta_file)
offset = meta_data.get('offset', 0)
except FileNotFoundError:
offset = 0
# 准备接收文件
response = {'status': 'ready', 'offset': offset} # 拓展功能代码:发送偏移量给客户端
client_socket.send(json.dumps(response).encode('utf-8'))
# 接收文件数据
try:
with open(file_path, 'ab') as f: # 拓展功能代码:以追加模式打开文件
f.seek(offset) # 拓展功能代码:移动文件指针到偏移量位置
bytes_received = offset
while bytes_received < filesize:
data = client_socket.recv(self.buffer_size)
if not data:
break
f.write(data)
bytes_received += len(data)
# 保存传输状态 拓展功能代码
with open(metadata_path, 'w') as meta_file:
json.dump({'offset': bytes_received}, meta_file)
# 上传完成,删除元数据文件 拓展功能代码
if os.path.exists(metadata_path):
os.remove(metadata_path)
response = {'status': 'success', 'message': f'文件 {filename} 上传成功'}
client_socket.send(json.dumps(response).encode('utf-8'))
except Exception as e:
# 上传失败,删除不完整文件
if os.path.exists(file_path):
os.remove(file_path)
if os.path.exists(metadata_path):
os.remove(metadata_path)
response = {'status': 'error', 'message': f'上传失败: {str(e)}'}
client_socket.send(json.dumps(response).encode('utf-8'))
def handle_download(self, client_socket, command):
"""处理文件下载"""
filename = command.get('filename')
offset = command.get('offset', 0) # 拓展功能代码:获取已传输字节偏移量
if not filename:
response = {'status': 'error', 'message': '缺少文件名'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
file_path = os.path.join(self.upload_dir, filename)
metadata_path = f"{file_path}.meta" # 拓展功能代码:元数据文件路径
# 检查文件是否存在
if not os.path.exists(file_path):
response = {'status': 'error', 'message': '文件不存在'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
filesize = os.path.getsize(file_path)
# 发送文件信息
response = {'status': 'success', 'filesize': filesize, 'offset': offset} # 拓展功能代码:发送偏移量给客户端
client_socket.send(json.dumps(response).encode('utf-8'))
# 等待客户端确认
confirmation_data = client_socket.recv(self.buffer_size).decode('utf-8')
confirmation = json.loads(confirmation_data)
if confirmation.get('status') != 'ready':
return
# 发送文件数据
try:
with open(file_path, 'rb') as f:
f.seek(offset) # 拓展功能代码:移动文件指针到偏移量位置
bytes_sent = offset
while bytes_sent < filesize:
data = f.read(self.buffer_size)
if not data:
break
client_socket.send(data)
bytes_sent += len(data)
# 保存传输状态 拓展功能代码
with open(metadata_path, 'w') as meta_file:
json.dump({'offset': bytes_sent}, meta_file)
# 下载完成,删除元数据文件 拓展功能代码
if os.path.exists(metadata_path):
os.remove(metadata_path)
except Exception as e:
print(f"发送文件失败: {str(e)}")
def handle_delete(self, client_socket, command):
"""处理文件删除"""
filename = command.get('filename')
if not filename:
response = {'status': 'error', 'message': '缺少文件名'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
file_path = os.path.join(self.upload_dir, filename)
metadata_path = f"{file_path}.meta" # 拓展功能代码:元数据文件路径
# 检查文件是否存在
if not os.path.exists(file_path):
response = {'status': 'error', 'message': '文件不存在'}
client_socket.send(json.dumps(response).encode('utf-8'))
return
# 尝试删除文件
try:
os.remove(file_path)
if os.path.exists(metadata_path):
os.remove(metadata_path) # 拓展功能代码:删除元数据文件
response = {'status': 'success', 'message': f'文件 {filename} 已删除'}
client_socket.send(json.dumps(response).encode('utf-8'))
except Exception as e:
response = {'status': 'error', 'message': f'删除文件失败: {str(e)}'}
client_socket.send(json.dumps(response).encode('utf-8'))
def main():
server = FileTransferServer()
server.start()
if __name__ == "__main__":
main()
6.2 client
import socket
import os
import json
import time
class FileTransferClient:
def __init__(self, host='localhost', port=9999, buffer_size=4096, download_dir='./client_files'):
self.host = host
self.port = port
self.buffer_size = buffer_size
self.download_dir = download_dir
self.client_socket = None
# 确保下载目录存在
if not os.path.exists(download_dir):
os.makedirs(download_dir)
def connect(self):
"""连接到服务器"""
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.client_socket.connect((self.host, self.port))
print(f"已连接到服务器 {self.host}:{self.port}")
return True
except Exception as e:
print(f"连接服务器失败: {e}")
return False
def disconnect(self):
"""断开与服务器的连接"""
if self.client_socket:
self.client_socket.close()
print("已断开与服务器的连接")
def list_files(self):
"""获取服务器文件列表"""
command = {'command': 'list'}
self.client_socket.send(json.dumps(command).encode('utf-8'))
response_data = self.client_socket.recv(self.buffer_size).decode('utf-8')
response = json.loads(response_data)
if response.get('status') == 'success':
files = response.get('files', [])
print("\n服务器文件列表:")
if not files:
print(" 空")
else:
for i, file in enumerate(files, 1):
print(f" {i}. {file}")
return files
else:
print(f"获取文件列表失败: {response.get('message')}")
return []
def upload_file(self, file_path):
"""上传文件到服务器"""
if not os.path.isfile(file_path):
print(f"文件不存在: {file_path}")
return
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
metadata_path = f"{file_path}.meta" # 拓展功能代码:元数据文件路径
# 检查是否有中断的传输 拓展功能代码
offset = 0
if os.path.exists(metadata_path):
try:
with open(metadata_path, 'r') as meta_file:
meta_data = json.load(meta_file)
offset = meta_data.get('offset', 0)
except FileNotFoundError:
offset = 0
command = {'command': 'upload', 'filename': filename, 'filesize': filesize, 'offset': offset}
self.client_socket.send(json.dumps(command).encode('utf-8'))
# 等待服务器确认
response_data = self.client_socket.recv(self.buffer_size).decode('utf-8')
response = json.loads(response_data)
if response.get('status') != 'ready':
print(f"服务器准备失败: {response.get('message')}")
return
server_offset = response.get('offset', 0) # 拓展功能代码:获取服务器返回的偏移量
if server_offset != offset:
offset = server_offset
# 发送文件数据
with open(file_path, 'rb') as f:
f.seek(offset) # 拓展功能代码:移动文件指针到偏移量位置
bytes_sent = offset
start_time = time.time()
while bytes_sent < filesize:
data = f.read(self.buffer_size)
if not data:
break
self.client_socket.send(data)
bytes_sent += len(data)
# 保存传输状态 拓展功能代码
with open(metadata_path, 'w') as meta_file:
json.dump({'offset': bytes_sent}, meta_file)
# 显示上传进度
progress = (bytes_sent / filesize) * 100
elapsed_time = time.time() - start_time
speed = bytes_sent / (elapsed_time + 0.001) / 1024 # KB/s
print(f"\r上传进度: {progress:.2f}% | 速度: {speed:.2f} KB/s", end='')
print("\n上传完成")
# 上传完成,删除元数据文件 拓展功能代码
if os.path.exists(metadata_path):
os.remove(metadata_path)
# 等待上传结果
response_data = self.client_socket.recv(self.buffer_size).decode('utf-8')
response = json.loads(response_data)
if response.get('status') == 'success':
print(response.get('message'))
else:
print(f"上传失败: {response.get('message')}")
def download_file(self, filename):
"""从服务器下载文件"""
download_path = os.path.join(self.download_dir, filename)
metadata_path = f"{download_path}.meta" # 拓展功能代码:元数据文件路径
# 检查是否有中断的传输 拓展功能代码
offset = 0
if os.path.exists(download_path):
if os.path.exists(metadata_path):
try:
with open(metadata_path, 'r') as meta_file:
meta_data = json.load(meta_file)
offset = meta_data.get('offset', 0)
except FileNotFoundError:
offset = 0
command = {'command': 'download', 'filename': filename, 'offset': offset}
self.client_socket.send(json.dumps(command).encode('utf-8'))
# 接收文件信息
response_data = self.client_socket.recv(self.buffer_size).decode('utf-8')
response = json.loads(response_data)
if response.get('status') != 'success':
print(f"下载失败: {response.get('message')}")
return
filesize = response.get('filesize')
server_offset = response.get('offset', 0) # 拓展功能代码:获取服务器返回的偏移量
if server_offset != offset:
offset = server_offset
# 发送准备好的确认
confirmation = {'status': 'ready'}
self.client_socket.send(json.dumps(confirmation).encode('utf-8'))
# 接收文件数据
with open(download_path, 'ab') as f: # 拓展功能代码:以追加模式打开文件
f.seek(offset) # 拓展功能代码:移动文件指针到偏移量位置
bytes_received = offset
start_time = time.time()
while bytes_received < filesize:
data = self.client_socket.recv(self.buffer_size)
if not data:
break
f.write(data)
bytes_received += len(data)
# 保存传输状态 拓展功能代码
with open(metadata_path, 'w') as meta_file:
json.dump({'offset': bytes_received}, meta_file)
# 显示下载进度
progress = (bytes_received / filesize) * 100
elapsed_time = time.time() - start_time
speed = bytes_received / (elapsed_time + 0.001) / 1024 # KB/s
print(f"\r下载进度: {progress:.2f}% | 速度: {speed:.2f} KB/s", end='')
print("\n下载完成")
print(f"文件已保存至: {download_path}")
# 下载完成,删除元数据文件 拓展功能代码
if os.path.exists(metadata_path):
os.remove(metadata_path)
def delete_file(self, filename):
"""删除服务器上的文件"""
command = {'command': 'delete', 'filename': filename}
self.client_socket.send(json.dumps(command).encode('utf-8'))
# 接收删除结果
response_data = self.client_socket.recv(self.buffer_size).decode('utf-8')
response = json.loads(response_data)
if response.get('status') == 'success':
print(f"文件 '{filename}' 已成功删除")
else:
print(f"删除文件失败: {response.get('message')}")
def main():
client = FileTransferClient()
if not client.connect():
return
try:
while True:
print("\n=== 文件传输系统 ===")
print("1. 查看服务器文件列表")
print("2. 上传文件到服务器")
print("3. 从服务器下载文件")
print("4. 删除服务器文件")
print("5. 退出")
choice = input("请选择操作 (1-5): ")
if choice == '1':
client.list_files()
elif choice == '2':
file_path = input("请输入要上传的文件路径: ")
client.upload_file(file_path)
elif choice == '3':
client.list_files()
file_num = input("请输入要下载的文件编号: ")
try:
file_num = int(file_num)
files = client.list_files()
if 1 <= file_num <= len(files):
client.download_file(files[file_num - 1])
else:
print("无效的文件编号")
except ValueError:
print("请输入有效的数字")
elif choice == '4':
client.list_files()
file_num = input("请输入要删除的文件编号: ")
try:
file_num = int(file_num)
files = client.list_files()
if 1 <= file_num <= len(files):
confirm = input(f"确定要删除文件 '{files[file_num - 1]}' 吗?(y/n): ")
if confirm.lower() == 'y':
client.delete_file(files[file_num - 1])
else:
print("无效的文件编号")
except ValueError:
print("请输入有效的数字")
elif choice == '5':
command = {'command': 'exit'}
client.client_socket.send(json.dumps(command).encode('utf-8'))
break
else:
print("无效的选择,请重新输入")
finally:
client.disconnect()
if __name__ == "__main__":
main()