irpas技术客

Python+Socket实现多人聊天室,功能:好友聊天、群聊、图片、表情、文件等_抱着西瓜吹空调_python socket聊天室

irpas 1886

一、项目简介

本项目主要基于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('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 五、私聊或评论告诉我,获取源码


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #Python #Socket聊天室 #登录注册 #添加好友 #与好友进行私聊 #创建群聊 #邀请申请加入群聊