环境说明
- 操作系统:mac
- 编程语言:Python3
- 依赖Python库:tkinter、pymysql、ssl、socket等(大多均为Python内置)
- mysql中应先建立数据库:filetransfer,新建表user,含有三个字段:id、username、password
生成证书
PROJECT_NAME="jiang zheng fool Project"
# Generate the openssl configuration files.
cat > ca_cert.conf << EOF
[ req ]
distinguished_name = req_distinguished_name
prompt = no
[ req_distinguished_name ]
O = $PROJECT_NAME Certificate Authority
EOF
cat > server_cert.conf << EOF
[ req ]
distinguished_name = req_distinguished_name
prompt = no
[ req_distinguished_name ]
O = $PROJECT_NAME
CN = SERVER
EOF
cat > client_cert.conf << EOF
[ req ]
distinguished_name = req_distinguished_name
prompt = no
[ req_distinguished_name ]
O = $PROJECT_NAME Device Certificate
CN = SERVER
EOF
mkdir ca
mkdir server
mkdir client
mkdir certDER
# private key generation
openssl genrsa -out ca.key 1024
openssl genrsa -out server.key 1024
openssl genrsa -out client.key 1024
# cert requests
openssl req -out ca.req -key ca.key -new -config ./ca_cert.conf
openssl req -out server.req -key server.key -new -config ./server_cert.conf
openssl req -out client.req -key client.key -new -config ./client_cert.conf
# generate the actual certs.
openssl x509 -req -in ca.req -out ca.crt -sha1 -days 5000 -signkey ca.key
openssl x509 -req -in server.req -out server.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key
openssl x509 -req -in client.req -out client.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key
mv ca.crt ca.key ca/
mv server.crt server.key server/
mv client.crt client.key client/
SSL协议提供的服务主要有:
1)认证用户和服务器,确保数据发送到正确的客户机和服务器;
2)加密数据以防止数据中途被窃取;
3)维护数据的完整性,确保数据在传输过程中不被改变。
生成证书文件如下:
打开ca.crt 可以看到如下图所示的结果:
概述
该项目主要有两个小项目组成,包括群聊功能,和文件传输系统。群聊功能主要实现了多人群聊,缓存消息等功能。文件传输系统包括文件的上传和下载。
群聊功能
程序启动
- client.py 客户端1
- client2.py 客户端2
- server.py 服务端
效果图
文件夹介绍
- cer -- 该文件夹存放了CA根证书及服务器、客户端证书(使用OpenSSL生成)
- CA -- 根证书及秘钥
- server -- 服务器秘钥、代签名证书及已签名证书
- client -- 客户端秘钥、代签名证书及已签名证书
文件介绍
MultiPersonChat
- client.py 客户端1
- client2.py 客户端2
- server.py 服务端
完成功能
- 上线提醒,下线提醒
- ssl链接
- 密码加密
- 用户不可重复登录
- server端需要缓存最近的部分历史消息,以便向刚上线的client时进行推送
- 可视化界面
- 多用户聊天
client.py Client类
| 方法名 | 具体功能 |
|---|---|
| send_login_info | 发送登录用户的用户名和密码给服务器验证,并return验证结果 |
| send_register_info | 发送用户注册的用户名和密码给服务器,并返回注册结果 |
| recv_data | 客户端向服务器接收数据 |
| close | 关闭客户端与服务器连接的套接字 |
client.py LoginPanel 类
| 方法名 | 具体功能 |
|---|---|
| set_panel_position | 设置登陆界面在屏幕中的位置和大小 |
| config_for_reg_panel | 给登陆界面设置其他配置 |
| set_title | 放置界面标题 |
| set_form | 放置登陆表单 |
| set_btn | 放置注册和登陆按钮 |
| show | 调用实例方法给登录界面做整体布局 |
| close | 实现对界面的关闭 |
| get_input | 获取用户输入的用户名和密码 |
| login_func | 封装到登陆界面中的登录按钮的功能 |
| reg_func | 封装到登录界面的注册按钮中,实现从登录界面跳转到注册界面 |
server.py
| 方法名 | 功能 |
|---|---|
| encrypt_psw | 使用 MD5 算法对用户的密码进行加密 |
| check_user | 检查用户登录时输入的用户名和密码是否正确 |
| add_user | 将要注册的用户名进行判断是否有重复用户名 |
| update_online_list | 更新客户端在线用户列表 |
| online_notice | 给所有在线客户端发送新客户端上线的通知 |
| offline_notice | 给所有在线用户发送用户离线通知 |
| handle_login | 处理登录请求 |
| handle_reg | 处理客户端的注册请求 |
| handle_msg | 对客户端要发送的内容进行广播 |
| handle | 服务器运行的主框架 |
关键代码如下,负责请求的分发:
def handle(new_socket, addr):
"""
服务器运行的主框架
:param new_socket: 本次连接的客户端套接字
:param addr: 本次连接客户端的ip和port
"""
try:
while True:
req_type = new_socket.recv(1).decode("utf-8") # 获取请求类型
print(req_type)
if req_type: # 如果不为真,则说明客户端已断开
if req_type == "1": # 登录请求
print("开始处理登录请求")
handle_login(new_socket)
elif req_type == "2": # 注册请求
print("开始处理注册请求")
handle_reg(new_socket)
elif req_type == "3": # 发送消息
print("开始处理发送消息请求")
handle_msg(new_socket)
else:
break
except Exception as ret:
print(str(addr) + " 连接异常,准备断开: " + str(ret))
finally:
try:
# 客户端断开后执行的操作
new_socket.close()
online_socket.remove(new_socket)
offline_notice(new_socket)
socket2user.pop(new_socket)
time.sleep(4)
update_online_list()
except Exception as ret:
print(str(addr) + "连接关闭异常")
缓存功能介绍
通过结构体把每一个聊天的内容和用户存储到数组当中,等用户登录的时候再分发给用户
文件安全传输功能
启动方法
- 启动服务器:
python server_ssl.py
python server_no_ssl.py
- 启动客户端:
python main.py
文件夹说明
- cer -- 该文件夹存放了CA根证书及服务器、客户端证书(使用OpenSSL生成)
- CA -- 根证书及秘钥
- server -- 服务器秘钥、代签名证书及已签名证书
- client -- 客户端秘钥、代签名证书及已签名证书
- ClientCache -- 该目录存放向服务器请求更新的下载列表数据
- ClientDownload -- 客户端下载路径
- ServerRec -- 服务器上传路径
文件说明
- main.py 客户端启动文件
- client_login.py 客户端登录界面
- client_mian.py 客户端主界面
- view.py 客户端主界面视图
- client_socket_no_ssl.py 客户端不加密通信对象
- client_socket_ssl.py 客户端加密通信对象
- server_no_ssl.py 服务器不加密通信代码
- server_ssl.py 服务器加密不加密通信代码
- result.txt 用来记录服务器的下载列表
- Serverlog.txt 服务器日志
流程图
服务端流程如下:
客户端流程图如下:
自定义传输协议
在服务器与客户端的每次交流都添加了自定义报头,客户端主动向服务器请求数据,服务器被动返回,因此两者的数据包头会不同。本系统使用了python的struct结构体实现了报头二进制流的传输。
客户端的报头内容如下:(1024字节)
- Command包括:Download、Upload、Update、Login和Register
- filename在download指令下是要下载的文件名,在Upload模式下是本地上传文件的路径。
- filesize是文件的大小
- time是数据请求的时间
- user和password是用户名和密码,每次请求数据都会验证一次,模拟Cookie模式。
header = {
'Command': 'Download',
'fileName': filename,
'fileSize': '',
'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
'user': self.username,
'password': self.password,
}
python中struct 用法如下
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('1024s', header_hex)
self.ssock.send(fhead)
服务器的报头内容如下:(128字节)
由于服务器是被动地回复客户端,所以报头内容不需要太多,故使用128字节。
- Feedback指示要回应的指令
- Stat指示响应的状态(如注册、登录等)
- Filesize是文件的大小
- User是当前用户
header = {
'Feedback': 'Login',
'stat': 'Success',
'fileSize': os.stat(listResult).st_size,
'user': username
}
关键代码
负责分发请求:
def conn_thread(self,connection):
while True:
try:
connection.settimeout(60)
fileinfo_size = struct.calcsize('1024s')
buf = connection.recv(fileinfo_size)
if buf: # 如果不加这个if,第一个文件传输完成后会自动走到下一句
header_json = str(struct.unpack('1024s', buf)[0], encoding='utf-8').strip('\00')
#print(header_json)
header = json.loads(header_json)
Command = header['Command']
if Command == 'Upload':
fileName = header['fileName']
fileSize = header['fileSize']
time = header['time']
user = header['user']
filenewname = os.path.join('ServerRec/', fileName)
print('Upload: file new name is %s, filesize is %s' % (filenewname, fileSize))
recvd_size = 0 # 定义接收了的文件大小
file = open(filenewname, 'wb')
print('start receiving...')
while not recvd_size == fileSize:
if fileSize - recvd_size > 1024:
rdata = connection.recv(1024)
recvd_size += len(rdata)
else:
rdata = connection.recv(fileSize - recvd_size)
recvd_size = fileSize
file.write(rdata)
file.close()
print('receive done')
fileSize = float(fileSize)
if fileSize<1024.0:
fileSize = "%s bytes"%(int(fileSize))
elif fileSize/1024.0 <= 1024.0:
fileSize = "%.2f Kb"%(fileSize/1024.0)
elif fileSize/1024.0/1024.0 <= 1024.0:
fileSize = "%.2f Mb"%(fileSize/1024.0/1024.0)
else:
fileSize = "%.2f Gb"%(fileSize/1024.0/1024.0/1024.0)
uploadmsg = '{"文件名": "%s", "上传者": "%s", "上传时间": "%s", "大小": "%s"}\n'%\
(fileName,user,time,fileSize)
with open('result.txt', 'a', encoding='utf-8') as list:
list.write(uploadmsg)
uploadlog = '\n%s upload a file "%s" at %s' % \
(user, fileName, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(uploadlog)
#connection.close()
elif Command == 'Login':
# 查询数据表数据
username = header['user']
password = header['password']
time = header['time']
sql = "select * from user where username = '%s' and password = '%s'"%(username,password)
cursor.execute(sql)
data = cursor.fetchone()
if data:
listResult = './result.txt'
# 定义文件头信息,包含文件名和文件大小
header = {
'Feedback': 'Login',
'stat': 'Success',
'fileSize': os.stat(listResult).st_size,
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
fo = open(listResult, 'rb')
while True:
filedata = fo.read(1024)
if not filedata:
break
connection.send(filedata)
fo.close()
print('%s login successfully')
loginlog = '\n%s try to login at "%s" , Stat: Success ' % \
(username, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(loginlog)
#connection.close()
else:
header = {
'Feedback': 'Login',
'stat': 'Fail',
'fileSize': '',
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
loginlog = '\n%s try to login at "%s" , Stat: Fail ' % \
(username, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(loginlog)
elif Command == 'Download':
# 查询数据表数据
username = header['user']
password = header['password']
time = header['time']
sql = "select * from user where username = '%s' and password = '%s'" % (username, password)
cursor.execute(sql)
data = cursor.fetchone()
filename = header['fileName']
if data:
filepath = os.path.join('./ServerREc/', filename)
# 定义文件头信息,包含文件名和文件大小
header = {
'Feedback': 'Download',
'stat': 'Success',
'fileSize': os.stat(filepath).st_size,
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
fo = open(filepath, 'rb')
while True:
filedata = fo.read(1024)
if not filedata:
break
connection.send(filedata)
fo.close()
print('send file over...')
downloadlog = '\n%s download a file "%s" at %s' % \
(username, filename, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(downloadlog)
# connection.close()
else:
header = {
'Feedback': 'Download',
'stat': 'LoginFail',
'fileSize': '',
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
elif Command == 'Update':
# 查询数据表数据
username = header['user']
password = header['password']
sql = "select * from user where username = '%s' and password = '%s'" % (username, password)
cursor.execute(sql)
data = cursor.fetchone()
if data:
listResult = './result.txt'
# 定义文件头信息,包含文件名和文件大小
header = {
'Feedback': 'Update',
'stat': 'Success',
'fileSize': os.stat(listResult).st_size,
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
fo = open(listResult, 'rb')
while True:
filedata = fo.read(1024)
if not filedata:
break
connection.send(filedata)
fo.close()
#print('send list over...')
# connection.close()
else:
header = {
'Feedback': 'Login',
'stat': 'Fail',
'fileSize': '',
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
elif Command == 'Register':
# 查询数据表数据
username = header['user']
password = header['password']
time = header['time']
sql = "select * from user where username = '%s'" % (username)
cursor.execute(sql)
data = cursor.fetchone()
if data:
# 定义文件头信息,包含文件名和文件大小
header = {
'Feedback': 'Register',
'stat': 'Exist',
'fileSize': '',
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
loginlog = '\n%s try to register at "%s" , Stat: Fail ' % \
(username, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(loginlog)
else:
sql = "insert into user values ('','%s','%s')"%(username,password)
cursor.execute(sql)
db.commit()
# 定义文件头信息,包含文件名和文件大小
header = {
'Feedback': 'Register',
'stat': 'Success',
'fileSize': '',
'user': username
}
header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('128s', header_hex)
connection.send(fhead)
loginlog = '\n%s try to register at "%s" , Stat: Success ' % \
(username, time)
with open('Serverlog.txt', 'a', encoding='utf-8') as list:
list.write(loginlog)
except socket.timeout:
connection.close()
break
except ConnectionResetError:
connection.close()
break
效果图
文件列表的效果图如下:
文件上传的效果图如下:
遇到问题
- 不会GUI :使用Python GUI编程(Tkinter)工具包
- 不会生成证书:openssl 命令
- hostname match error:
context.wrap_socket(self.sock, server_hostname='SERVER', server_side=False)中的server_hostname要和server.conf 中的CN保持一致
实验收获与体会
这次的实验让我学到了很多东西,让我对 SSL 加密传输和 TCP 协议有了更深的了 解。以前都是在书上看到的 TCP 协议知识,这次通过自己抓包,验证了 TCP 的三次握 手、ACK 号等等,让我对计算机网络知识更加感兴趣了。此外,这次实验自己设计协议, 自己申请 SSL 证书、实现 GUI 等都是以前没有试过的,完成之后成就感非常大,对相 关领域的理解又加深了一点。 此次实验遇到的问题主要有 GUI 的设计、SSL 证书的申请等。因为事前不是很了 解,所以开发时比较难处理,后来经过大量查阅资料、逛博客、学习别人代码,终于把 困难一一解决,成功实现了所有功能。这次的实验锻炼了我思考和解决问题的能力,也 增加了自己的经验,非常充实。