目录
第一章 项目简介及源代码链接
1.1 源代码链接
https://github.com/chenaaa1/-python.git
1.2 项目基本功能
- 利用socket方式编写一个多人聊天室程序,可以实现多个用户之间的群聊功能,私聊功能,显示当前用户功能
- 在聊天室程序中增加利用ftp实现文件的上传,下载,删除,查看当前文件功能
- 在聊天室程序中增加利用ftp实现在聊天过程中可以发送图片功能
- 在聊天室程序中增加发送邮件功能,并提供附件发送服务
第二章 功能演示
- 设置好服务端的配置代码
- 登录界面
- 使用过程
第三章 模块化分析
3.1 服务端
服务端主要用于为客户端提供聊天服务和FTP服务,所以我分别创建了两个类为ChatServer和FtpServer
3.1.1 服务端基本设置
对于服务端的代码分析,首先是要让用户对服务器的基本信息进行设置,具体设置代码如下
# ----------------------------------------文件传输服务器1--------------------------
class FtpServer():
# 新建一个用户组
authorizer = DummyAuthorizer()
# 将用户名,密码,指定目录,权限 添加到里面
authorizer.add_user(username_server, password_server, ftp_catalogue, perm=user_power) # adfmw
# 这个是添加匿名用户,任何人都可以访问,如果去掉的话,需要输入用户名和密码,可以自己尝试
authorizer.add_anonymous(ftp_catalogue)
handler = FTPHandler
handler.authorizer = authorizer
def run(self):
# 开启服务器
server = FTPServer((IP_server, FTP_PORT), self.handler)
server.serve_forever()
# ----------------------------------------文件传输服务器2--------------------------
3.1.2 聊天服务器主要模块
- TCP连接模块,主要代码就是与客户端建立起TCP通信
- 当前在线用户模块,主要就对当前连接的客户端数目进行判断,然后返回一个当前在线用户数
- 接收信息模块,主要代码是接收客户端发来的信息,然后存入一个队列中
- 发送信息模块,主要代码是对客户端发来的信息进行分析,分析完后转入对于的函数去执行相关操作,比如接收到的是聊天信息就发到聊天用户的客户端中,接收到的是图片信息就发送指定字符串给聊天用户的客户端,然后对指定图片执行FTP上传下载操作。
- 服务器启动检查模块,主要是令服务端与客户端进行连接,然后在控制台内打印连接状况的信息。
3.1.3 FTP服务器设置模块
对于FTP服务器,主要代码是用于初始化FTP服务器,如设置FTP的用户,密码和FTP的文件夹,具体如下图所示
# ----------------------------------------文件传输服务器1--------------------------
class FtpServer():
# 新建一个用户组
authorizer = DummyAuthorizer()
# 将用户名,密码,指定目录,权限 添加到里面
authorizer.add_user(username_server, password_server, ftp_catalogue, perm=user_power) # adfmw
# 这个是添加匿名用户,任何人都可以访问,如果去掉的话,需要输入用户名和密码,可以自己尝试
authorizer.add_anonymous(ftp_catalogue)
handler = FTPHandler
handler.authorizer = authorizer
def run(self):
# 开启服务器
server = FTPServer((IP_server, FTP_PORT), self.handler)
server.serve_forever()
# ----------------------------------------文件传输服务器2--------------------------
3.2 客户端
3.2.1 初始登录模块
初始登录模块,即用tkinter库创建出一个界面,再在里面添加输入框,最后将用户输入的信息保存为对应的全局变量给后续代码使用。核心代码如下所示
# 登录按钮
def login(*args):
global IP, PORT, user, ftpUser, ftpPassword
IP, PORT = entryIP.get().split(':') # 获取IP和端口号
PORT = int(PORT) # 端口号需要为int类型
user = entryUser.get()
ftpUser = entryFtpUser.get()
ftpPassword = entryFtpPassword.get()
if not user:
tkinter.messagebox.showerror('错误', message='请输入用户名')
else:
root1.destroy() # 关闭登录窗口
3.2.2 聊天窗口模块
聊天窗口模块,即登录成功后为用户显示出来的窗口,用于用户聊天和进行其他操作,主要也是利用了tkinter库来创建出对应的界面
3.2.3 图片发送模块
(1)先进行FTP登录,方便后续把图片发送到服务器中
(2)打开本地文件资源管理器,找到图片的位置,修改图片的大小,因为聊天窗口无法加载过大的图片 (3)利用FTP把图片上传到服务器中
(4)给服务器发送一个指定的消息,表示这是一个发送图片命令
3.2.4显示当前在线用户模块
主要原理是先接收服务端发来的当前在线用户数,然后插入到聊天窗口中即可。
3.2.5与服务器通信模块
主要思路是先根据窗口信息确定聊天对象,然后再根据用户输入信息确定聊天内容,最后将这些信息加上分隔符后拼接成字符串,然后发送给服务端。核心代码如下
def send(*args):
# 没有添加的话发送信息时会提示没有聊天对象
users.append('------群聊模式-------')
print(chat)
if chat not in users:
tkinter.messagebox.showerror('Send error', message='There is nobody to talk to!')
return
if chat == user:
tkinter.messagebox.showerror('Send error', message='Cannot talk with yourself in private!')
return
mes = entry.get() + ':;' + user + ':;' + chat # 添加聊天对象标记
s.send(mes.encode())
a.set('') # 发送后清空文本框
3.2.6私聊模块
主要思路是确定好聊天的两个对象,然后将用户和其聊天对象名拼接成字符串显示到聊天框中,方便用户确定当前聊天对象,核心代码如下
def private(*args):
global chat
# 获取点击的索引然后得到内容(用户名)
indexs = listbox1.curselection()
index = indexs[0]
if index > 0:
chat = listbox1.get(index)
# 修改客户端名称
if chat == '------群聊模式-------':
root.title(user + ' 的群聊窗口')
return
# 下面这两行用于在标题头处显示目前是谁和谁在聊天
ti = user + ' 与 ' + chat + ' 的聊天窗口'
root.title(ti)
3.2.7邮件功能模块
主要思路如下
global fileName_mail_img
global fileName_mail_file
global fileName_mail_text
global fileName_mail_video
# 然后给对应变量赋值即可
sender = entry_sender.get()
# sender = input('请输入一个电子邮箱地址(163/QQ):')
username, domain = sender.split('@')
if domain == '163.com':
host = 'smtp.163.com'
elif domain == 'qq.com':
host = 'smtp.qq.com'
else:
# print('当前代码只识别163和QQ邮箱,请检查邮箱地址或修改代码。')
exit()
port = 25
body = entry_mail_body.get("1.0", "end") # 这个get里面的参数是获得所有信息的
# 输入密码,无回显,需要在cmd或PowerShell运行程序
userpwd = entry_sender_passwd.get()
# 要群发的电子邮件地址
recipients = entry_recipients.get()
# 登录邮箱服务器
with SMTP(host, port) as server:
server.starttls()
server.login(username, userpwd)
# 创建邮件
msg = MIMEMultipart()
msg.set_charset('utf-8')
# 回复地址与发信地址可以不同
# 但是大部分邮件系统在回复时会提示
msg['Reply-to'] = sender
# 设置发信人、收信人和主题
msg.add_header('From', sender)
msg.add_header('To', recipients)
msg.add_header('Subject', entry_mail_topic.get())
msg['Date'] = formatdate()
msg['Message-Id'] = make_msgid()
# 设置邮件文字内容
msg.attach(MIMEText(body, 'plain', _charset='utf-8'))
# ---------------------------------这里为添加附件功能-------------------
# 首先是要打开一个窗口,然后获取到对应的文件名
# 添加图片
fn = fileName_mail_img
print(fn)
if fn :
with open(fn, 'rb') as fp:
attachment = MIMEImage(fp.read())
attachment.add_header('content-disposition',
'attachment', filename=fn.split('/')[-1])
msg.attach(attachment)
# 添加文本文件
fn = fileName_mail_text
print(fn)
if fn:
with open(fn, 'rb') as fp:
attachment = MIMEBase('text', 'txt')
attachment.set_payload(fp.read())
encode_base64(attachment)
attachment.add_header('content-disposition',
'attachment',
filename=fn.split('/')[-1])
msg.attach(attachment)
# 添加可执行程序
fn = fileName_mail_file
print(fn)
if fn:
with open(fn, 'rb') as fp:
attachment = MIMEApplication(fp.read(),
_encoder=encode_base64)
attachment.add_header('content-disposition',
'attachment', filename=fn.split('/')[-1])
msg.attach(attachment)
# 添加音乐文件
fn = fileName_mail_video
print(fn)
if fn:
with open(fn, 'rb') as fp:
attachment = MIMEAudio(fp.read(), 'plain',
_encoder=encode_base64)
attachment.add_header('content-disposition',
'attachment', filename=fn.split('/')[-1])
msg.attach(attachment)
# 发送邮件
server.send_message(msg)
# 每次发完清空附件
fileName_mail_file = ''
fileName_mail_text = ''
fileName_mail_video = ''
fileName_mail_img = ''
3.2.8 FTP功能模块
主要思路如下
global now_page
# 将打印出来的数据截取出对应的文件名(这个有点巧妙)
dir_res = []
ftp.dir('.', dir_res.append) # 对当前目录进行dir(),将结果放入列表
# print(dir_res)
print(len(dir_res)) # 打印列表长度
# 由文件名判断名称是文件还是目录
for i in range(14):
# 防止数组越界
if (now_page*14)+i <= len(dir_res)-1:
name = dir_res[(now_page*14)+i].split(' ')[-1]
# print(name)
# 将名称插入列表
list2.insert(tkinter.END, name)
# 通过.号判断是否为文件或文件夹,从而赋予不同的颜色(暂时未测试过文件夹)
if '.' not in name:
list2.itemconfig(tkinter.END, fg='orange')
else:
list2.itemconfig(tkinter.END, fg='blue')
(1)提供文件上传或文件夹上传服务,如果是文件就直接调用FTP的函数上传,如果是文件夹就先切换到该文件夹中,再遍历其中的文件并依次上传到FTP服务器中。上传文件夹的核心代码如下
def upload_ftpdir():
# 思路:先获取到对应文件名,然后以二进制形式上传,ftp的目录信息是在服务端进行设置的
# 获取文件夹名,并判断ftp目录里是否存在该文件夹
fileName_ftp = tkinter.filedialog.askdirectory()
fileName_ftp_end = fileName_ftp.split('/')[-1]
dir = []
ftp.dir('.', dir.append) # 对当前目录进行dir(),将结果放入列表
# 将打印信息拼接成字符串,方便后面的not in 判断
rubbish = ''
for i in dir:
rubbish = rubbish + i
# print(rubbish)
if fileName_ftp_end not in rubbish:
# 不存在则创建文件夹
ftp.mkd(fileName_ftp_end)
# 然后进行下载
ftp.cwd(fileName_ftp_end)
dir_res = []
files = os.listdir(fileName_ftp)
print(files)
for i in files:
dir_res.append(i)
print(dir_res)
for name in dir_res:
print(fileName_ftp + '/' + name)
fd = open(fileName_ftp + '/' + name, 'rb')
# 以二进制的形式上传
ftp.storbinary("STOR %s" % name, fd)
fd.close()
else:
ftp.cwd(fileName_ftp_end)
dir_res = []
files = os.listdir(fileName_ftp)
print(files)
for i in files:
dir_res.append(i)
print(dir_res)
# ftp.dir(fileName_ftp_end, dir_res.append) # 对当前目录进行dir(),将结果放入列表
# print(dir_res)
for name in dir_res:
print(fileName_ftp + '/' + name)
fd = open(fileName_ftp + '/' + name, 'rb')
# 以二进制的形式上传
ftp.storbinary("STOR %s" % name, fd)
fd.close()
# 传完文件后再把目录切回原目录,再刷新界面
ftp.cwd('..')
# 上传完文件后刷新一遍目录窗口,首先要清空目录,再重载
list2.delete(0, tkinter.END) # 清空列表框
show_ftp()
print("upload finished")
(2)提供下载文件服务,主要思路是先获取用户在聊天窗口中选中的文件名称,然后用这个文件名称去FTP服务器进行查找,最后将其下载到指定文件夹中,如果用户选择下载的是文件夹,那就先切换到文件夹中,再依次遍历其中的文件并下载到指定目录中。核心代码如下
def download_ftp():
# 获取到要下载到本地的文件名(只需要名字就行)
fileName_ftp = list2.get(list2.curselection())
print(fileName_ftp)
# 对文件名做判断
if '.' not in fileName_ftp:
# 如果是文件夹的话就切换ftp目录进该文件夹,再下载,最后返回源目录
ftp.cwd(fileName_ftp)
dir_res = []
# 将目录内文件信息保存到dir_res中
ftp.dir('.', dir_res.append) # 对当前目录进行dir(),将结果放入列表
# print(dir_res)
# 从dir_res中取出文件名放入files数组中
files = []
for i in dir_res:
files.append(i.split(' ')[-1])
# print(files)
# 设置文件存储路径(路径为文件夹类型)
dir_path = tkinter.filedialog.askdirectory()
os.mkdir(dir_path + '\\' + fileName_ftp)
# print(dir_path_end)
for j in files:
# 是文件的话就直接下载
# 这里要对目录路径进行一些小替换以符合格式
dir_path_end = dir_path.replace("/", '\\') + '\\' + fileName_ftp + '\\' + j
fd = open(dir_path_end, 'wb')
# 以二进制形式下载,注意第二个参数是fd.write,上传时是fd
ftp.retrbinary("RETR %s" % j, fd.write)
fd.close()
ftp.cwd('..')
else:
# 是文件的话就直接下载
# 设置文件存储路径(路径为文件夹类型)
dir_path = tkinter.filedialog.askdirectory()
# 这里要对目录路径进行一些小替换以符合格式
dir_path_end = dir_path.replace("/", '\\') + '\\' + fileName_ftp
print(dir_path_end)
fd = open(dir_path_end, 'wb')
# 以二进制形式下载,注意第二个参数是fd.write,上传时是fd
ftp.retrbinary("RETR %s" % fileName_ftp, fd.write)
fd.close()
print("download finished")
(3)提供删除FTP服务器内文件服务。主要思路是先获取用户在聊天窗口中选中的文件名称,然后判断文件是单个文件还是文件夹,最后调用相应的FTP函数进行删除。核心代码如下
def delete_ftp():
# 获取到要下载到本地的文件名(只需要名字就行)
fileName_ftp = list2.get(list2.curselection())
print(fileName_ftp)
# 通过.号判断是否为文件或文件夹,从而赋予不同的颜色
if '.' not in fileName_ftp:
# 因为rmd命令只能删空目录
# 所以我们要先递归删除目录里的文件
dir_res = []
ftp.dir(fileName_ftp, dir_res.append) # 对当前目录进行dir(),将结果放入列表
print(dir_res)
for i in dir_res:
name = i.split(' ')[-1]
print(name)
ftp.delete(fileName_ftp + '/' + name)
ftp.rmd(fileName_ftp)
else:
ftp.delete(fileName_ftp)
print("delete finished")
# 上传完文件后刷新一遍目录窗口,首先要清空目录,再重载
list2.delete(0, tkinter.END) # 清空列表框
show_ftp()
3.2.9接收服务端发来的信息模块
(1)首先判断接收到的信息是否抛出异常,如果没有捕获到异常则接收到的是当前在线用户列表,则取出其中的当前在线用户名并依次插入到聊天窗口中。
(2)然后判断接收到的信息的指定位置是否含有特殊词“image”,如果有的话代表是发送图片的命令,则连接上FTP服务器并根据图片名称下载到本地,然后在本地打开该图片并加载到聊天窗口中。
(3)如果以上判断均不成立,则为普通的聊天信息。然后判断聊天对象,最后把聊天内容发送给对应的聊天对象的客户端即可。