一、项目简介
本项目主要基于python实现的多人聊天室,主要的功能如下:
登录注册添加好友与好友进行私聊创建群聊邀请/申请加入群聊聊天发送图片聊天发送表情聊天发送文件聊天记录保存在本地中聊天过程中发送的文件保存本地二、环境介绍
python3.8mysql8.0tkinter:作为程序的gui库flask :主要用于登录/注册、表情下载、信息修改等http请求等socket:主要用户聊天过程中消息发送、对方在线状态更新等pygame:用于播放新消息提示音三、运行展示
登录:
注册:
登录后主界面:
点击右上方“修改资料”:
添加好友或群:
双击好友或群打开聊天窗口:
点击表情按钮选择发送的表情:
发送图片可以预览,点击文件名称直接打开:
四、关键代码
配置文件:server.conf
配置服务器ip、http端口、socket端口、数据库的账号密码、是否启用新消息提示音
[server]
SERVER_IP = 127.0.0.1
HTTP_PORT = 8000
SOCKET_PORT = 8001
SQLALCHEMY_DATABASE_URI = mysql://root:root@127.0.0.1:3306/chatdb
ENABLE_MUSIC = 0
服务端主要代码:ChatServer.py
维持Socket通信、开启Flask进行http
# controller定义
@app.route(/login, methods=[POST])
def login():
try:
params = request.values
login_name = params[loginName]
pwd = params[pwd]
md5 = hashlib.md5()
md5.update(pwd.encode(encoding=utf-8))
password = md5.hexdigest()
users = Users.query.filter(Users.loginName == login_name)\
.filter(Users.pwd == password).all()
if len(users) == 0:
return Result.fail(账号不存在或密码错误)
else:
# 服务返回uid,客户端打开好友界面后,凭借此uid与服务器进行socket连接
uid = users[0].id
# 已存在uid:已登录,重新登录,原登录退出连接,退出程序
if uid in online_users.keys():
# logout
connection = online_users[int(uid)]
send_msg = {type: UtilsAndConfig.SYSTEM_LOGOUT}
connection.send(json.dumps(send_msg).encode())
online_users[uid] = None
return Result.success(uid)
except Exception as e:
return Result.fail(参数异常)
# 监听socket
def socket_listen_thread():
while True:
connection, address = mySocket.accept()
# 用户连接携带的uid,判断是否和服务器相同
data_dic = json.loads(connection.recv(1024).decode())
uid = None
if data_dic[type] == UtilsAndConfig.CONNECTION_REQUEST:
uid = int(data_dic[uid])
else:
connection.send(UtilsAndConfig.CONNECTION_NOT_ALLOWED.encode())
if uid in online_users.keys():
# 可建立连接
online_users[uid] = connection
connection.send(UtilsAndConfig.CONNECTION_ALLOWED.encode())
# 通知好友们,我上线了
friends = get_friends_by_uid(uid)
for f in friends:
if f.id in online_users.keys():
friend_connection = online_users[f.id]
send_msg = {type: UtilsAndConfig.FRIENDS_ONLINE_CHANGED, uid: uid, online: 1}
friend_connection.send(json.dumps(send_msg).encode())
# 创建子线程,保持通信
keep_link_thread = threading.Thread(target=socket_keep_link_thread, args=(connection, ))
keep_link_thread.setDaemon(True)
keep_link_thread.start()
else:
connection.send(UtilsAndConfig.CONNECTION_NOT_ALLOWED.encode())
def socket_keep_link_thread(connection):
while True:
try:
msg = connection.recv(1024).decode()
if not msg:
if connection in online_users.values():
uid = list(online_users.keys())[list(online_users.values()).index(connection)]
online_users.pop(uid)
friends = get_friends_by_uid(uid)
for f in friends:
if f.id in online_users.keys():
friend_connection = online_users[f.id]
send_msg = {type: UtilsAndConfig.FRIENDS_ONLINE_CHANGED, uid: uid, online: 0}
friend_connection.send(json.dumps(send_msg).encode())
connection.close()
return
else:
msg_json = json.loads(str(msg))
# 发消息
if msg_json[type] == UtilsAndConfig.CHAT_SEND_MSG:
to_id = msg_json[toId]
is_friend = msg_json[isFriend]
from_uid = msg_json[fromId]
send_time = msg_json[sendTime]
msg_text = msg_json[msgText]
data = {from_uid: from_uid, to_id: to_id, send_time: send_time, msg_text: msg_text,
is_friend: is_friend, type: , msg_type: train}
# 通知接收方,收到新消息
if is_friend == 1:
if to_id in online_users.keys():
friend_connection = online_users[to_id]
data[type] = UtilsAndConfig.CHAT_HAS_NEW_MSG
friend_connection.send(json.dumps(data).encode())
# 通知发送方,发送成功
data[type] = UtilsAndConfig.CHAT_SEND_MSG_SUCCESS
connection.send(json.dumps(data).encode())
else:
# 通知发送方,发送失败,对方不在线
data[type] = UtilsAndConfig.CHAT_SEND_MSG_ERR
connection.send(json.dumps(data).encode())
else:
# 群
members = get_group_members(to_id)
members_online = False
for m in members:
if m.uId in online_users.keys() and m.uId != from_uid:
members_online = True
member_connection = online_users[m.uId]
data[type] = UtilsAndConfig.CHAT_HAS_NEW_MSG
member_connection.send(json.dumps(data).encode())
if members_online:
# 通知发送方,发送成功
data[type] = UtilsAndConfig.CHAT_SEND_MSG_SUCCESS
connection.send(json.dumps(data).encode())
else:
# 通知发送方,发送失败,对方不在线
data[type] = UtilsAndConfig.CHAT_SEND_MSG_ERR
connection.send(json.dumps(data).encode())
if msg_json[type] == UtilsAndConfig.CHAT_SEND_FILE:
from_id = msg_json[from_id]
to_id = msg_json[to_id]
is_friend = msg_json[is_friend]
send_date = msg_json[send_date]
file_length = msg_json[file_length]
file_suffix = msg_json[file_suffix]
file_name = msg_json[file_name]
file_save_name = str(uuid.uuid1()) + . + file_suffix
return_file_path = /static/tmp/ + file_save_name
file_path = os.path.abspath(os.path.dirname(__file__)) + return_file_path
if not os.path.exists(os.path.dirname(file_path)):
os.makedirs(os.path.dirname(file_path))
data = {from_uid: from_id, to_id: to_id, send_time: send_date, file_name: file_name,
is_friend: is_friend, type: UtilsAndConfig.CHAT_SEND_FILE_SUCCESS,
file_path: return_file_path}
if is_friend == 1:
if to_id not in online_users.keys():
# 通知发送方,发送失败,对方不在线
data[type] = UtilsAndConfig.CHAT_SEND_MSG_ERR
connection.send(json.dumps(data).encode())
continue
else:
members = get_group_members(to_id)
flag = True
for m in members:
if m.uId in online_users.keys() and m.uId != from_id:
flag = False
break
if flag:
# 通知发送方,发送失败,对方不在线
data[type] = UtilsAndConfig.CHAT_SEND_MSG_ERR
connection.send(json.dumps(data).encode())
continue
# 接收文件
total_data = b
file_data = connection.recv(1024)
total_data += file_data
num = len(file_data)
while num < file_length:
file_data = connection.recv(1024)
num += len(file_data)
total_data += file_data
with open(file_path, “wb”) as f:
f.write(total_data)
connection.send(json.dumps(data).encode())
# 通知接收方,收到新文件消息
if is_friend == 1:
friend_connection = online_users[to_id]
data[type] = UtilsAndConfig.CHAT_HAS_NEW_FILE
friend_connection.send(json.dumps(data).encode())
else:
members = get_group_members(to_id)
for m in members:
if m.uId in online_users.keys() and m.uId != from_id:
member_connection = online_users[m.uId]
data[type] = UtilsAndConfig.CHAT_HAS_NEW_FILE
member_connection.send(json.dumps(data).encode())
except ConnectionAbortedError:
if connection in online_users.values():
uid = list(online_users.keys())[list(online_users.values()).index(connection)]
online_users.pop(uid)
friends = get_friends_by_uid(uid)
for f in friends:
if f.id in online_users.keys():
friend_connection = online_users[f.id]
send_msg = {type: UtilsAndConfig.FRIENDS_ONLINE_CHANGED, uid: uid, online: 0}
friend_connection.send(json.dumps(send_msg).encode())
connection.close()
return
except ConnectionResetError:
if connection in online_users.values():
uid = list(online_users.keys())[list(online_users.values()).index(connection)]
online_users.pop(uid)
friends = get_friends_by_uid(uid)
for f in friends:
if f.id in online_users.keys():
friend_connection = online_users[f.id]
send_msg = {type: UtilsAndConfig.FRIENDS_ONLINE_CHANGED, uid: uid, online: 0}
friend_connection.send(json.dumps(send_msg).encode())
connection.close()
return
# 主线程
if __name__ == __main__:
# 启动socket线程
socketThread = threading.Thread(target=socket_listen_thread)
socketThread.setDaemon(True)
socketThread.start()
# 启动Flask服务器
app.run(host=serverConfig.SERVER_IP, port=serverConfig.HTTP_PORT, debug=False)
客户端主界面:ChatHome.py
与服务器保持Socket通信、与服务端进行http交互
class ChatHome:
def run(self):
pygame.mixer.init()
# Socket连接
self.socket.connect((self.server_config.SERVER_IP, self.server_config.SOCKET_PORT))
send_data = {type: UtilsAndConfig.CONNECTION_REQUEST, uid: self.uid}
self.socket.send(json.dumps(send_data).encode())
socket_result = self.socket.recv(1024).decode()
if socket_result != UtilsAndConfig.CONNECTION_ALLOWED:
tkinter.messagebox.showwarning(提示, 参数出错,socket连接被拒绝!)
sys.exit()
# 创建子线程保持socket通信
keep_link_thread = threading.Thread(target=self.socket_keep_link_thread)
keep_link_thread.setDaemon(True)
keep_link_thread.start()
# 基本信息
self.root = tk.Tk()
self.root.title(ChatRoom)
self.root.geometry(320×510+100+0)
# 用户名
self.frame_user_info = Frame(self.root, relief=RAISED, width=320, borderwidth=0, height=70, bg=#4F7DA4)
self.frame_user_info.place(x=0, y=0)
self.init_user_info()
# 中间画布canvas
self.frame_mid = Frame(self.root, width=320, height=340)
self.frame_mid.place(x=0, y=70)
# # 画布中的frame
self.init_friends_and_group_view()
# 下方按钮
frame_bottom_button = Frame(self.root, relief=RAISED, borderwidth=0, width=320, height=50)
frame_bottom_button.place(x=0, y=420)
button_bottom_add_friends = Button(frame_bottom_button, width=11,
text=加好友/加群, command=self.open_add_friends)
button_bottom_add_friends.place(x=55, y=10)
button_bottom_create_groups = Button(frame_bottom_button, width=11,
text=创建群, command=self.open_create_groups)
button_bottom_create_groups.place(x=165, y=10)
# 新消息
frame_message = Frame(self.root, relief=RAISED, borderwidth=0, width=320, height=50)
frame_message.place(x=0, y=460)
self.label_message_tip = Label(frame_message)
self.label_message_tip.place(x=55, y=12)
self.refresh_message_count()
button_message_open = Button(frame_message, width=7,
text=查看, command=self.open_message_window)
button_message_open.place(x=193, y=10)
self.root.mainloop()
# 保持socket通信
def socket_keep_link_thread(self):
while True:
try:
back_msg = self.socket.recv(1024).decode()
msg = json.loads(back_msg)
# 好友状态改变
if msg[type] == UtilsAndConfig.FRIENDS_ONLINE_CHANGED:
self.frames_friend_view[msg[uid]].online_type_change(msg[online])
# 有新验证消息
if msg[type] == UtilsAndConfig.MESSAGE_NEW_MSG:
self.refresh_message_count()
self.play_new_msg_music()
# 好友/群数量改变
if msg[type] == UtilsAndConfig.FRIENDS_GROUPS_COUNT_CHANGED:
self.init_friends_and_group_view()
self.refresh_message_count()
# 有新文本消息, 写入缓存,更新显示
if msg[type] == UtilsAndConfig.CHAT_HAS_NEW_MSG:
from_uid = msg[from_uid]
to_id = msg[to_id]
is_friend = msg[is_friend]
txt = {type: get, from_uid: from_uid, datetime: msg[send_time],
msg: msg[msg_text], msg_type: train}
UtilsAndConfig.add_one_chat_record(self.uid, is_friend, from_uid, to_id,
json.dumps(txt, cls=UtilsAndConfig.MyJSONEncoder,
ensure_ascii=False), False)
# 是否打开聊天界面,打开则更新,未打开则好友列表提示新消息
if self.window_chat_context is not None and self.window_chat_context.to_id == from_uid\
and self.window_chat_context.is_friend == 1 and is_friend == 1:
self.window_chat_context.get_new_msg()
pass
elif self.window_chat_context is not None and self.window_chat_context.to_id == to_id\
and self.window_chat_context.is_friend == 0 and is_friend == 0:
self.window_chat_context.get_new_msg()
else:
if is_friend == 1:
self.frames_friend_view[from_uid].new_msg_comming()
else:
self.frames_group_view[to_id].new_msg_comming()
self.play_new_msg_music()
# 发送文本消息成功, 写入本地缓存,更新显示
if msg[type] == UtilsAndConfig.CHAT_SEND_MSG_SUCCESS:
from_uid = msg[from_uid]
to_id = msg[to_id]
send_time = msg[send_time]
msg_text = msg[msg_text]
is_friend = msg[is_friend]
txt = {type: send, datetime: send_time, msg: msg_text, msg_type: train}
UtilsAndConfig.add_one_chat_record(self.uid, is_friend, from_uid, to_id,
json.dumps(txt, cls=UtilsAndConfig.MyJSONEncoder,
ensure_ascii=False), True)
self.window_chat_context.get_new_msg()
# 发送文件成功
if msg[type] == UtilsAndConfig.CHAT_SEND_FILE_SUCCESS:
to_id = msg[to_id]
send_time = msg[send_time]
file_name = msg[file_name]
is_friend = msg[is_friend]
txt = {type: send, datetime: send_time, msg: file_name, msg_type: file}
UtilsAndConfig.add_one_chat_record(self.uid, is_friend, self.uid, to_id,
json.dumps(txt, cls=UtilsAndConfig.MyJSONEncoder,
ensure_ascii=False), True)
self.window_chat_context.get_new_msg()
self.window_chat_context.sending_file(False)
# 收到文件
if msg[type] == UtilsAndConfig.CHAT_HAS_NEW_FILE:
to_id = msg[to_id]
from_uid = msg[from_uid]
send_time = msg[send_time]
file_name = msg[file_name]
is_friend = msg[is_friend]
file_path = msg[file_path]
files_dir = os.path.abspath(os.path.dirname(__file__)) + /static/LocalCache/ \
+ str(self.uid) + /files/
if not os.path.exists(os.path.dirname(files_dir)):
os.makedirs(os.path.dirname(files_dir))
all_file_name = file_name.split(/)[–1]
file_suffix = all_file_name.split(.)[–1]
end_index = len(all_file_name) – len(file_suffix) – 1
file_name = all_file_name[0:end_index]
file_save_path = files_dir + file_name + . + file_suffix
i = 1
while os.path.exists(file_save_path):
file_save_path = files_dir + file_name + ( + str(i) + ) + . + file_suffix
i += 1
# http下载文件,保存到本地
try:
url = self.server_config.HTTP_SERVER_ADDRESS + file_path
res = requests.get(url)
file_content = res.content
file = open(file_save_path, wb)
file.write(file_content)
file.close()
except requests.exceptions.InvalidSchema:
pass
# 服务器中文件不存在
txt = {type: get, from_uid: from_uid, datetime: send_time,
msg: file_save_path, msg_type: file}
UtilsAndConfig.add_one_chat_record(self.uid, is_friend, from_uid, to_id,
json.dumps(txt, cls=UtilsAndConfig.MyJSONEncoder,
ensure_ascii=False), False)
if self.window_chat_context is not None and self.window_chat_context.to_id == from_uid\
and self.window_chat_context.is_friend == 1 and is_friend == 1:
self.window_chat_context.get_new_msg()
pass
elif self.window_chat_context is not None and self.window_chat_context.to_id == to_id\
and self.window_chat_context.is_friend == 0 and is_friend == 0:
self.window_chat_context.get_new_msg()
else:
if is_friend == 1:
self.frames_friend_view[from_uid].new_msg_comming()
else:
self.frames_group_view[to_id].new_msg_comming()
self.play_new_msg_music()
# 告诉服务器 文件下载完成,可删除
url = self.server_config.HTTP_SERVER_ADDRESS + /downloadFileSuccess?path= + file_path
requests.get(url)
# 发送聊天消息失败,不写入缓存,提示对方已下线
if msg[type] == UtilsAndConfig.CHAT_SEND_MSG_ERR:
tkinter.messagebox.showwarning(提示, 对方已下线,不能发送消息)
# 服务器强制下线
if msg[type] == UtilsAndConfig.SYSTEM_LOGOUT:
self.socket.close()
tkinter.messagebox.showwarning(提示, 此账号已在别处登录!)
self.root.destroy()
return
except ConnectionAbortedError:
tkinter.messagebox.showwarning(提示, 与服务器断开连接!)
self.root.destroy()
return
except ConnectionResetError:
tkinter.messagebox.showwarning(提示, 与服务器断开连接!)
self.root.destroy()
return
五、私聊或评论告诉我,获取源码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END