Python+Socket实现多人聊天室,功能:好友聊天、群聊、图片、表情、文件等_python socket聊天室

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    一、项目简介

    本项目主要基于python实现的多人聊天室主要的功能如下:

    • 登录注册
    • 添加好友
    • 与好友进行私聊
    • 创建群聊
    • 邀请/申请加入群聊
    • 聊天发送图片
    • 聊天发送表情
    • 聊天发送文件
    • 聊天记录保存在本地中
    • 聊天过程中发送的文件保存本地

    二、环境介绍

    • python3.8
    • mysql8.0
    • tkinter:作为程序的gui库
    • flask :主要用于登录/注册、表情下载、信息修改等http请求等
    • socket:主要用户聊天过程中消息发送、对方在线状态更新等
    • pygame:用于播放新消息提示音

    三、运行展示

    登录:
    登录页面
    注册:
    image-20220427221842887
    登录后主界面:
    image-20220427221858349
    点击右上方“修改资料”:
    image-20220427221942224
    添加好友或群:
    image-20220427222002416
    双击好友或群打开聊天窗口:
    image-20220427222032094
    点击表情按钮选择发送的表情:
    image-20220427222102584
    发送图片可以预览点击文件名称直接打开:
    image-20220427222217499

    四、关键代码

    配置文件: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
    

    五、私聊或评论告诉我获取源码

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
    标签: python