毛毛Python进阶之路2——实现双端互联
1、两个月前我开始自学Python
2、一个月前我在室友的压迫下,我用Python优秀的第三方库requests库完成了相对正规的爬虫项目。可以做到批量爬去网络上美眉的图片【手动滑稽!!!】
链接:https://blog.csdn.net/qq_42874244/article/details/82855171 ————》 打开探索之门
3、终于,在昨天!不对,是今天凌晨两点(头发估计要没了),我完成我的又一个相对以前更正规的项目——“云端v2.0”。话说为什么是2.0.了?因为以前做1.0的时候我的知识量太少了,虽然也完成了项目,可自己也是糊里糊涂的,BUG还多!没办法!但现在的2.0就不一样了!看官接招!
一、需要准备的:
- Python当然必不可少啦!(我是用的pycharm)
- Python优秀的第三方库socke库(对!TA就是主角!)
- 其他Python内置模块os、hashlib、json、sys
- 其实本次项目最大的亮点——“面向对象编程”(我一直以来都只会面向过程!这次的面向对象可真是把我难到了,那也是几个晚上没睡觉才搞懂了那么一丢丢!)
二、项目需要实现的功能:
- 客户端可以登录、注册。
- 服务端可以实现一对多。
- 每个用户有自己的家目录。
- 上传文件到云端
- 下载文件到本地
三、具体的实现:
思路:
1、在开始编程前我们考虑到我们的服务端是个服务类型的,所以我们应该从client端开始编程然后提示server端应该给我们提供哪些帮助!
2、既然是一个项目就应该“褪去小白最青涩的外套”,不再在一个文件里面写完所有的程序,而是将文件整理做到“锅碗瓢盆既是一家,又有分类!”,所以我们应该按需求创建“bin—运行程序”“conf—配置文件”“core—程序主体”“db—用户信息”“log—输出日志”,当然server端肯定要一个“home—用户家目录”。这样的haul每个文件就可以“该干嘛干嘛去”了。
3、由于我还不会前端开发,和client的交互只能是命令行的形式,所以我们不应该让用户输入太多,这样用户体验不好,所以我们做出来的功能只需要让用户选择输入我们需要的简单符号即可,比如“123456789abc”。
4、整体过程应该是:
四、程序源代码
client 端代码
1、start.py
# 运行程序
import os
import sys
sys.path.append(os.path.dirname(os.getcwd()))
from core import client
if __name__ == '__main__':
client.main()
2、core 里面的 clien.py
from core.Auth_client import Auth
def main():
print("欢迎来到 “云端V2.0”!\n请选择:")
print("=" * 50)
start_1 = [("登录","login"),("注册","register"),("退出","quit")]
for index,item in enumerate(start_1,1):
print(index,item[0])
# 用 enumerate 可以序列化列表,得到选项输出
# 现在处理相关功能 由于功能较多 而且以后可能会添加 所以采取函数调用的方式调用相关函数!
# 先写 Auth 函数
while True:
auth_obj = None
try:
print("=" * 50)
num = int(input("请输入你的选项:")) # 直接转为int型可能会出错!
print("=" * 50)
func_str = start_1[num - 1][1]
except:
print("输入错误!")
# 利用反射就可以不考虑用户选择的功能了 直接反射到相应的功能
if hasattr(Auth, func_str):
auth_obj = Auth()
func = getattr(auth_obj, func_str)
ret,username = func() # 在这里得到一个用户名字,在下面功能执行时需要!
# 在执行 exit() 函数时要考虑 auth.obj 是否还和服务端连接着
elif auth_obj:
auth_obj.socket.sk.close()
exit()
else:
exit()
if ret:
while True:
# 接着执行第二层!功能选择!
auth_obj = None
print("=" * 50)
print("请选择功能:")
start_2 = [("进入家目录","go_home",),("下载文件","download"),("上传文件","upload"),("退出系统","quit")]
for index1,item2 in enumerate(start_2,1):
print(index1,item2[0]) # 用同样的方法
try:
print("=" * 50)
num = int(input("请输入你的选项:")) # 直接转为int型可能会出错!
print("="*50)
func_str = start_2[num - 1][1]
except:
print("输入错误!")
# 利用反射就可以不考虑用户选择的功能了 直接反射到相应的功能
if hasattr(Auth, func_str):
auth_obj = Auth()
func = getattr(auth_obj, func_str)
func(username)
elif auth_obj:
auth_obj.socket.sk.close()
exit()
else:
exit()
3、core里面的socke_cilent.py
# 这个类是用来和server端来建立连接并收发消息!
import socket
# server端接受字典时好处理数据,所以采用json包装发送的数据!
import json
class MySocketClient:
def __init__(self):
self.sk = socket.socket()
self.sk.connect(("127.0.0.1",8000))
def mysend(self,msg):
send_json = json.dumps(msg)
self.sk.send(send_json.encode("utf-8"))
def myrecv(self):
ret = self.sk.recv(1024)
ret = ret.decode("utf-8")
ret = json.loads(ret)
return ret
def myrecvfile(self,x):
ret = self.sk.recv(x)
return ret
def mysendfile(self,msg):
self.sk.send(msg)
4、core里面的Auth_client.py
# 目前所需要解决的是 登录 和 注册
from core.socket_client import MySocketClient
import os
import hashlib
class Auth:
# 建立连接只需要一次就可以 所以采取 “单例模式”
__instance = None
def __new__(cls, *args, **kwargs):
if not cls.__instance:
obj = object.__new__(cls)
obj.socket = MySocketClient()
obj.username = None
cls.__instance = obj
return cls.__instance
def login(self):
print("现在是登录程序!")
username = input("username : ")
password = input("password : ")
# 在某些操作系统里面如果传入空到服务端可能会报错!
if username.strip() and password.strip():
# 考虑到这个文件是功能合集 需要多次向 server端 多次发送消息,而且还要建立连接 所以新建立一个类来处理这方面函数!
self.socket.mysend({"username":username,"password":password,"operation":"login"})
ret = self.socket.myrecv()
if ret == "操作成功!":
return True,username # 在这里得到一个用户名字,在下面功能执行时需要!
else:
return False,None
def register(self):
print("现在是注册程序!")
username = input("username : ")
password1 = input("password : ")
password2 = input("password_confirm : ")
# 同样的 传入空会出错 且要保证密码一样!
if username.strip() and password1.strip() and password1 == password2:
self.socket.mysend({"username": username,"password":password1,"operation":"register"})
ret = self.socket.myrecv()
if ret == "操作成功!":
return True
else:
return False
def go_home(self,username):# 进入家目录需要用户名字 通过函数传过来 一起发送到server端。
print("进入家目录成功,以下为您的文件!")
self.socket.mysend({"username":username,"key":"go_home"})
ret = self.socket.myrecv() # 这里返回的是一个列表,所以序列化一下
print("="*50)
for i in ret:
print(i)
print("="*50)
return True
def download(self,username):
print("您选择了下载文件!")
while True:
content = input("请输入 “get 文件名”的形式:")
if content.startswith("get"): # 判断字符串里面是否有 get
self.socket.mysend({"username":username,"key":"download","content":content})
# 1、先接受长度
server_reaponse = self.socket.myrecvfile(1024)
file_size = int(server_reaponse.decode("utf-8"))
print("接收文件大小为:",file_size)
# 2、接受文件内容
self.socket.mysendfile("准备好接受".encode("utf-8"))
filename = "new" + content.split(" ")[1]
os.chdir("..")
os.chdir("downfile")
f = open(filename, "wb")
received_size = 0
m = hashlib.md5()
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > 1024: # 多次接收
size = 1024
else: # 最后一次接收完毕
size = file_size - received_size
data = self.socket.myrecvfile(size)
data_len = len(data)
received_size += data_len
print("已接收:",int(received_size/file_size), "%")
m.update(data)
f.write(data)
f.close()
print("实际接收的大小:", received_size) # 解码
# 3、md5值校验
md5_sever = self.socket.myrecvfile(1024).decode("utf-8")
md5_client = m.hexdigest()
print("服务器发来的md5:", md5_sever)
print("接收文件的md5:", md5_client)
if md5_sever == md5_client:
print("MD5值校验成功")
else:
print("MD5值校验失败")
print("=" * 50)
print("文件下载成功!")
print("=" * 50)
return True
else:
print("输入格式错误!")
def upload(self,username):
print("您选择了上传文件!")
while True:
print("要求:\n1、请按照格式“Send 文件名”发送文件。\n2、请将需要发送的文件放在根目录下的Sendfile目录下!\n3、承认本程序作者比你帅!")
content = input("请输入 “Send 文件名”的形式:")
filename = content.split(" ")[1]
if content.startswith("Send"): # 同上 判断字符串里面是否有按照格式来!
self.socket.mysend({"username": username, "key": "upload", "namefile": filename})
f = self.socket.myrecv()
print(f)
# 1.先发送文件大小,让客户端准备接收
os.chdir("..")
os.chdir("sendfile")
size = os.stat(filename).st_size # 获取文件大小
self.socket.mysendfile(str(size).encode("utf-8")) # 发送数据长度
print("发送的大小:", size)
# 2.发送文件内容
self.socket.myrecvfile(1024) # 接收确认
m = hashlib.md5()
f = open(filename, "rb")
for line in f:
self.socket.mysendfile(line) # 发送数据
m.update(line)
f.close()
# 3.发送md5值进行校验
md5 = m.hexdigest()
self.socket.mysendfile(md5.encode("utf-8")) # 发送md5值
print("md5:", md5)
print("文件已发送至云端!")
return True
else:
print("输入格式错误!")
以上基本就是client端所有代码,具体目录如下图所示:
server 端代码
1、start.py 运行程序
这里采用socketserver可以实现一对多!
import os
import sys
sys.path.append(os.path.dirname(os.getcwd()))
from core.server import MyTCPServer
import socketserver
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(("127.0.0.1",8000),MyTCPServer)
server.serve_forever()
2、conf 下的 setting.py
这里是配置文件!
import os
addr = ("127.0.0.1",8000)
code = "utf-8"
os.chdir("..")
os.chdir("home")
home_path = os.getcwd()
space = 1024*1024*2
os.chdir("..")
os.chdir("db")
pickle_path = os.getcwd() + "\\user_pickle"
user_info = os.getcwd() + "\\userinfo"
3、core 下的 server.py
import socketserver
import json
import os
import hashlib
from core import views
class MyTCPServer(socketserver.BaseRequestHandler):
def handle(self):
while True:
msg = self.myrecv()
try:
os_str = msg["operation"]
if hasattr(views,os_str):
func = getattr(views,os_str)
ret = func(msg)
if ret:
ret = "操作成功!"
else:
ret = "操作失败!"
self.mysend(ret)
except:
# 我们不将地址写死,也就是说server端不管在哪里我们都可以用这个程序 于是通过 os.chdir() 将程序执行路径定格在 home 因为我们后面的程序就只是在home里面执行!
print("选择程序——", os.getcwd())
os_str = msg["key"]
if hasattr(views,os_str):
func = getattr(views,os_str)
ret = func(msg)
if os_str == "download":#在这里由于我无法直接在views里面调用server的send 和 recv所以我直接将程序搬到server里面来了!
content = msg["content"]
filename = content.split(" ")[1]
size = os.stat(filename).st_size
# 1、发送文件大小,让客户端准备接受!
abc = str(size).encode("utf-8")
self.mysendfile(abc) #发送数据长度
print("发送的大小",size)
# 2、发送文件内容
self.myrecvfile()
m = hashlib.md5()
for line in ret:
self.mysendfile(line)
m.update(line)
ret.close()
# 3、发送ms5值进行校验!
md5 = m.hexdigest()
self.mysendfile(md5.encode("utf-8"))
print("md5 = ",md5)
# self.mysendfile(ret) 无法解决粘包问题
print("用户下载文件成功——————》")
os.chdir("..")
elif os_str == "upload":
username = msg["username"]
filename = msg["namefile"]
self.mysend("正在创建文件…………")
# 1.先接收长度,建议8192
server_response = self.request.recv(1024)
file_size = int(server_response.decode("utf-8"))
print("接收到的大小:", file_size)
# 2.接收文件内容
self.request.send("准备好接收".encode("utf-8")) # 接收确认
filename = "new" + filename
f = open(filename, "wb")
received_size = 0
m = hashlib.md5()
while received_size < file_size:
size = 0 # 准确接收数据大小,解决粘包
if file_size - received_size > 1024: # 多次接收
size = 1024
else: # 最后一次接收完毕
size = file_size - received_size
data = self.request.recv(size) # 多次接收内容,接收大数据
data_len = len(data)
received_size += data_len
print("已接收:", int(received_size / file_size * 100), "%")
m.update(data)
f.write(data)
md5_client = self.myrecvfile()
md5_server = m.hexdigest()
if md5_server == md5_client:
print("MD5值校验成功")
else:
print("MD5值校验失败")
f.close()
os.chdir("..")
print("上传文件结束——", os.getcwd())
elif os_str == "go_home":
self.mysend(ret)
else:
print("程序出错!————————》")
self.mysend("程序出错!")
#在这里发现json不支持字节的转换 所以我在下面又定义了mysendfile 和 myrecvfile 函数来实现文件发送与接收!
def myrecv(self):
msg = self.request.recv(1024)
msg = msg.decode("utf-8")
msg = json.loads(msg)
return msg
def mysend(self,msg):
msg = json.dumps(msg)
msg = msg.encode("utf-8")
self.request.send(msg)
def mysendfile(self,msg):
self.request.send(msg)
def myrecvfile(self):
msg = self.request.recv(1024)
return msg
4、core 下的 views.py
import os
import pickle
from core.user import User
from conf import settings
def login(msg):
print("用户在登录————————————")
try:
key = False
username = msg["username"]
password = msg["password"]
with open(settings.user_info,"r") as f:
for i in f:
Split = str(i).split("|")
if username == Split[0] and password == Split[1]:
key = True
return key
except Exception as e:
print("程序出错:",e)
return False
def register(msg):
''' 注册的时候我们应该
1、写入文件账号|密码
2、创建用户家目录
3、记录用户初始磁盘配额
4、当前磁盘大小 '''
print("用户在注册————————————")
try:
user_obj = User(msg["username"])
pickle_path = os.path.join(settings.pickle_path,msg["username"])
with open(pickle_path,"wb") as f:
pickle.dump(user_obj,f)
os.makedirs(user_obj.home)
with open(settings.user_info,'a') as f:
f.write("%s|%s|%s\n"%(msg["username"],msg["password"],pickle_path))
return True
except:
return False
def go_home(msg):
print("用户进入了家目录—————————")
print("家目录位置——",os.getcwd())
try:
os.chdir("..")
os.chdir("home") # 拼接路径进入用户家目录
username = msg["username"]
catalog = os.listdir(username) # 列出用户目录有些什么并返回给用户
if not catalog:
catalog = "账户【%s】云端没有任何文件!你可以选择上传来满足它!" %username
print("家目录退出位置——", os.getcwd())
return catalog
except Exception as e:
print(e)
return False
def download(msg):
print("用户开始下载文件—————————")
print("用户下载文件位置——", os.getcwd())
try:
os.chdir("..")
os.chdir("home")
username = msg["username"]
content = msg["content"]
filename = content.split(" ")[1]
os.chdir(username)
fo = open(filename,"rb")
# for i in fo: # 在这里不能直接返回fo,因为此时需要的信息不是以字节流的形式储存在fo 里面的而是 i/o 的形式在内存里面!
# x = i
# fo.close()
# os.chdir("..")
print("下载文件结束——", os.getcwd())
return fo
except Exception as e:
print(e)
return False
def upload(msg):
print("用户开始上传文件————————")
print("上传文件开始——", os.getcwd())
try:
username = msg["username"]
namefile = msg["namefile"]
os.chdir("..")
os.chdir("home")
os.chdir(username)
open(namefile,"wb")
return True
except Exception as e:
print(e)
return False
5、core 下的 user.py
这个是以后会实现对用户磁盘配额!
import os
from conf import settings
class User:
def __init__(self,name):
self.name = name #用户名
self.home = os.path.join(settings.home_path,name) # 拼接家目录路径!
self.cur_parh = self.home
self.disk_space = settings.space # 配置的磁盘配额大小
self.file_size = 0 # 目前家目录已经用了多少!
这就是server端的全部代码,下图是server端的目录
以上就是 “云端v2.0” 的全部代码!后续我添加新功能后会有更新!
总结一下:
在学习的时候听很多人过来人说“光说不练假本事!”,学习一门新的编程语言光有理论知识是不够的,多练习,多做项目,才能真正的提升自己的水平。有的人觉得做项目需要耗费大量的时间,但Ta并不知道做一个项目所获得的,是他永远无法在其他人那里学到的!知识点固然重要,但更重要的是经验,遇事不乱的那种心态!
最后,希望大家看了这篇文章有所收获!
人生苦短,我学Python!