本项目主要基于python实现的多人聊天室,主要的功能如下:
登录:
注册:
登录后主界面:
点击右上方“修改资料”:
添加好友或群:
双击好友或群打开聊天窗口:
点击表情按钮选择发送的表情:
发送图片可以预览,点击文件名称直接打开:
配置文件: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('320x510+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