一.Design ideas
【1】运行条件
一.pycharm(拟假设方案名称为 demo1)目录下
1.server.py (或a1.py)
2.client.py (或a2.py)
3.新建python包 utils包
(1)该包自动生成__init__.py
(2)该包新建 server-utils.py client-utils.py
(3)该包新建 tools.py 服务器与客户端共同使用函数
二.初使化服务器和客户端的目录 (utils.__init__.py)
1.服务器目录 方案目录\server 方案目录\server\share
2.客户端目录
(1)方案目录\client\download (从服务器下载文件放在此处)
(2)方案目录\client\upload (要上传服务器文件放在此处)
【2】服务器与客户端的程序分开编写 不可混用
server.py(服务器主程序) utils.serverUtil.py(配套工具)
client.py(客户端主程序) utils.clientUtil.py
一.初使化server服务器和client客户端
1.socket对象 设置参数 ipv4 tcp ip端口可以复用
2.绑定ip端口 最大连接数 服务器上传下载的共享目录
二.发送接收命令
1.如客户端发送命令 put a2.py
2.服务器接收命令
3.发送接收格式(双方约定的通信过程)
(1)客户端发送 <=1024 个字节的信息 = 客户端 发送命令信息 的通信
(2)服务端接收 >=1024 个字节的信息 = 服务器 接收命令信息 的通信
三.客户端上传文件至服务器 客户端发送 服务器接收 发送格式如下
1.头信息字典 序列化 再转二进制流 计算头信息长度固定4个字节
(1)head_dic ={"filename":a2.py , "md5":md5, "file_size":file_size}
(2)序列化成字符串二进制流 json.dumps ( head_dic ).encode("gbk")
(3)struct.pack("i",head_bytes)
2.通信过程(接收方角度)
(1)先接收4字节(解析头部信息的长度)
(2)接收头部信息(解析出文件的长度)
(3)接收文件信息
【3】总结
1.客户端上传文件的通信
2.客户端下载文件的通信
二.pyProjects/demo1 项目方案
(一) demo1/utils package包
r"""
python首行的<多行注释> 最好前面带上r 代表不转义字符 否则可能报错
pyproject/demo1(项目名称/utils/__init__.py
"""
#coding:utf8
import os
class Singleton:
projectDir= os.path.dirname( os.path.dirname(__file__) )
def __init__(self):
self.executed = False
#【思考点】去掉该执行一次的标志 每次导入该包时就会执行一次
#【问题】只执行一次 如果解决误删除的操作呢?
def execute_once(self):
if not self.executed:
#print("多次导入该模块 但有效的代码块只执行一次")
self.executed = True
dirs = [r"\server", r"\server\share",
r'\client',
r'\client\download', r'\client\upload']
for dir in dirs:
self.create_dir(Singleton.projectDir+dir)
def create_dir(self,dirpath):
if not os.path.exists(dirpath):
os.mkdir(dirpath)
Singleton().execute_once()
if __name__ == '__main__':
Singleton().execute_once() #【思考点】可以单独测试该python程序
r""" utils/tools.py """
import os,hashlib
def get_file_md5(file_abs_path):
m = hashlib.md5()
with open(file_abs_path, "rb") as f:
for line in f.readlines():
m.update(line)
return m.hexdigest()
# 【解决】接收文件时 会获取该文件名称 保存该文件时可能与本地文件重名
# 如遇有重名a1.txt的文件命名格式为 a1(1).txt a1(2).txt
def saveFileName(saveDir, filename:str):
filename=filename.strip()
if not os.path.exists(saveDir + os.sep + filename):
return filename
fs = os.path.splitext(filename) #如 (a1,txt)
i, tag = 1, fs[0] + "({})" + fs[1] #如 a1(1).txt
while True:
f = tag.format(i)
if not os.path.exists(saveDir + os.sep + f):
return f
else:
i += 1
if __name__ == '__main__': ##【此处可以测试程序】
with open("a1.txt",mode="w",encoding="utf8")as f:
f.write("123456")
print(get_file_md5("a1.txt")) #a1.txt保存二进流信息与txt程序
print(hashlib.md5("123456".encode("utf8")).hexdigest())
print( saveFileName(".","a1.txt") )
print( saveFileName(".","no_exit.py" ) )
r""" utils/serverUtil.py """
import os, struct, json, hashlib
import socket
from utils.tools import get_file_md5,saveFileName
#from tools import get_file_md5,saveFileName
# 服务器主程序运行 为什么会报错?因为包的功能?
#服务器与客户端的代码模块 一定要分别编写(即使代码有重复)
class ServerSend:
"""
【思考点】ServerReceive对象可以调用方法和属性
[属性] socketObj file_absPath (__init__)
statcode file_name (send方法生成的属性)
[方法] send
"""
def __init__(self,socketObj:socket.socket,file_absPath):
self.socketObj=socketObj
self.file_absPath=file_absPath
self.send()
def send(self):
#服务器需要一个状态码 客户端接收时需要获取状态码
#要发送文件存在 服务器发送状态码200(表示请求资源存在 主信息发送)
#发送文件不存在 服务器发送状态码404(表示请求资源不存在 主信息不发送)
if os.path.exists(self.file_absPath):
statcode = "200"
filename = os.path.basename(self.file_absPath)
md5 = get_file_md5(self.file_absPath)
file_size=os.path.getsize(self.file_absPath)
else:
statcode="404"
#表客户端在浏览网页时,服务器无法正常提供消息
filename,md5,file_size="","",0
header_dic = {"statcode":statcode,"filename": filename,
"md5": md5,"file_size": file_size}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode("gbk")
header_len = struct.pack("i", len(header_bytes))
self.socketObj.send(header_len) # 1.4个字节
self.socketObj.send(header_bytes) # 2.头部二进制信息
self.statcode=statcode
self.file_name=filename
if statcode=="200":
with open(self.file_absPath, "rb") as f:
for line in f: # 3.主数据信息
self.socketObj.send(line)
else:
print(
"[警告]客户端{}请求服务端{}资源{}不存在".format(
self.socketObj.getpeername(),
self.socketObj.getsockname(),filename)
)
class ServerReceive:
"""
【思考点】ServerReceive对象可以调用方法和属性
[属性] socketObj saveDir (__init__初使化属性)
file_len file_name md5 (receive方法生成属性)
saveFileMD5 saveFilePath
[方法] receive
"""
def __init__(self, socketObj:socket.socket, saveDir):
self.socketObj = socketObj
self.saveDir = saveDir
self.receive()
def receive(self):
header_len_bytes = self.socketObj.recv(4)
#print(len(header_len_bytes),header_len_bytes) #【测试点】
header_len = struct.unpack("i", header_len_bytes)[0]
header_bytes = self.socketObj.recv(header_len)
header_dict = json.loads(header_bytes.decode("gbk"))
self.file_len = header_dict["file_size"]
self.file_name = header_dict["filename"]
self.md5 = header_dict["md5"]
saveFilename = saveFileName(self.saveDir, self.file_name)
savaFilePath = self.saveDir + os.sep + saveFilename
saveFileMD5 = hashlib.md5()
with open(savaFilePath, "wb") as f:
recv_size = 0
while recv_size < self.file_len:
line = self.socketObj.recv(1024)
saveFileMD5.update(line)
f.write(line)
recv_size += len(line)
self.saveFileMD5 = saveFileMD5.hexdigest()
self.saveFilePath = os.path.abspath(savaFilePath)
if __name__ == '__main__':
print( [m for m in dir(ServerReceive) if m in ["__init__","receive"] ] )
""" utils/clientUtils.py """
import os, struct, json, hashlib
import socket
from utils.tools import get_file_md5,saveFileName
#from tools import get_file_md5,saveFileName
class ClientReceive:
def __init__(self, socketObj: socket.socket, saveDir=None):
self.socketObj = socketObj
self.saveDir = saveDir
self.receive()
def receive(self):
header_len_bytes = self.socketObj.recv(4)
print("test 客户端接收数据:",header_len_bytes)
header_len = struct.unpack("i", header_len_bytes)[0]
#服务器突然断开此处就会报错 因上述接收为空值
self.head_len = header_len
header_len = self.head_len
header_bytes = self.socketObj.recv(header_len)
header_dict = json.loads(header_bytes.decode("gbk"))
statcode=header_dict["statcode"]
if str(statcode)=="404": #"服务器请求资源不存在"
self.md5,self.saveFileMD5,self.saveFilePath="","",""
return
self.file_len = header_dict["file_size"]
self.file_name = header_dict["filename"]
self.md5 = header_dict["md5"]
saveFilename = saveFileName(self.saveDir, self.file_name)
savaFilePath = self.saveDir + os.sep + saveFilename
saveFileMD5 = hashlib.md5()
with open(savaFilePath, "wb") as f:
recv_size = 0
while recv_size < self.file_len:
line = self.socketObj.recv(1024)
saveFileMD5.update(line)
f.write(line)
recv_size += len(line)
self.saveFileMD5 = saveFileMD5.hexdigest()
self.saveFilePath = os.path.abspath(savaFilePath)
class ClientSend: #不用发送状态码
def __init__(self, socketObject:socket.socket, file_absPath):
self.socketObj = socketObject
self.send(file_absPath)
def send(self, file_absPath):
filename = os.path.basename(file_absPath)
self.md5 = get_file_md5(file_absPath)
header_dic = {
"filename": filename,
"md5": self.md5,
"file_size": os.path.getsize(file_absPath)
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode("gbk")
header_len = struct.pack("i", len(header_bytes))
self.socketObj.send(header_len)
self.socketObj.send(header_bytes)
with open(file_absPath, "rb") as f:
for line in f:
self.socketObj.send(line)
(二) demo1/server.py (a1.py)
#coding:utf8
import multiprocessing,os,socket,sys
import threading
from utils.serverUtils import ServerSend,ServerReceive
class Server(object): #Server=TCPServer_FileTransferSystem简称
share_dir=r".\server\share"
server_ip_port=("127.0.0.1",9999) #(localhost,9999)
client_connection_num=5
max_pack_size=1024
def __init__(self):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(Server.server_ip_port)
s.listen(Server.client_connection_num)
self.server=s
def __enter__(self): # with关键词的开始执行
print("服务器已启动...")
self.clientObj=dict()
def __exit__(self, exc_type, exc_val, exc_tb): # with 结束执行
# exclude execute ExceptionType/Value/Trackback
print("服务器已关闭...")
self.server.close()
sys.exit()
def run(self): #启动多进程 服务 多个客户端
while True: #服务器永远不关闭 服务器报错才关闭
conn, client_addr =self.server.accept()
print("【客户端IP端口(%s,%s)】"%tuple(client_addr), "该客户端已连接!!")
client_process = multiprocessing.Process(
target=self.client_handle,
args=(conn,client_addr),
name="【客户端IP端口(%s,%s)】" % (client_addr[0], client_addr[1]))
#【思考点】改为线程更好 设置进程或线程 进程或线程也都需要启动
#client_process=threading.Thread(target,args,name)#
self.clientObj[client_addr] = client_process.name
client_process.start()
def client_handle(self,conn,client_addr): #处理一个客户端
with conn:
while True:
cmds = conn.recv(self.max_pack_size)
print("接收客户端的命令:",cmds.decode()) #测试是否正确接收
cmds = cmds.decode().strip().split()
if len(cmds) == 0: # "".split=[]
print("客户端已断开...")
conn.close()
break
cmd = str(cmds[0]) # 1.命令 get put
filename = cmds[1] # 2.参数 filename
try:
if hasattr(self, cmd): # get/put/是否有错误 如输入__get_md5
execute_fun = getattr(self, cmd) # 获取cmd名称(函数)地址
execute_fun(conn, filename)
else:
print("输入命令不正确")
pass # 提示错误信息
except ConnectionResetError as e:
print( self.clientObj[client_addr],"该客户端异常,将断开其连接..." )
raise
def get(self,conn:socket.socket,filename): #获取客户端 get命令 启动发送
filePath = self.share_dir + os.sep + filename
s=ServerSend(conn, filePath)
filename=s.file_name.strip()
if filename!="":
print(f"服务器端已将文件{filename}发送给客户端 {conn.getpeername()}")
else:
pass #【测试点】客户端 get no_exit.py
def put(self,conn:socket.socket,filename=None): #获取客户端put上传命令 服务端接收
obj=ServerReceive(conn,self.share_dir)
print("服务器端接收到客户端上传的文件,保存在 %s" % obj.saveFilePath)
print( "该文件的md5值为%s 上传文件自带md5值为%s"%( obj.saveFileMD5,obj.md5) )
if __name__ == '__main__':
server=Server()
with server:
server.run()
(三) demo1/client.py (a2.py)
import socket, os,sys
from utils.clientUtils import ClientSend,ClientReceive
class Client(object): # ClientTCPFileTransferSystem
download_dir = r'.\client\download'
upload_dir = r'.\client\upload'
server_share_dir = r".\server\share"
server_ip_port = ("127.0.0.1", 9999)
max_pack_size = 1024
def __init__(self):
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.connect(self.server_ip_port)
def __enter__(self):
print("客户端已启动..")
def __exit__(self, exc_type=None,exc_val=None, exc_tb=None):
print("客户端已关闭..")
self.client.close()
sys.exit()
def run(self):
while True:
cmdstr = input(">>: ").strip()
if not cmdstr: continue # 不能输入空字符串
cmds=cmdstr.split()
if len(cmds)!=2 or cmds[0] not in ["get","put"]:
print("get/put+一个文件名称(含扩展名),请重新输入...")
continue
self.filename = cmds[1]
try:
self.client.send( cmdstr.encode() )
getattr(self, cmds[0])() # fun=getattr(self,cmd);fun()
except:
print("【报错】服务器突然断开 此处将报错....")
break #客户端代码运行完毕 with自动进入self.__exit__退出
def get(self):
obj = ClientReceive(self.client, self.download_dir)
if not obj.md5:
print("请求服务器的资源不存在...")
return
if obj.saveFileMD5 == obj.md5:
print("Files md5 is consistent: %s" % obj.md5)
print("Successfully file is downloaded!!")
print("The path to save the file is %s" % obj.saveFilePath)
def put(self):
fname = self.upload_dir + os.sep + self.filename
if not os.path.exists(fname):
print(f"【警告】{self.upload_dir}目录中无此文件{self.filename},请检查!!")
return
ClientSend(self.client, fname)
print("文件已上传成功!!")
if __name__ == '__main__':
client = Client()
with client:
client.run()