Python+Socket实现多人聊天室,功能:好友聊天、群聊、图片、表情、文件等
作者:mmseoamin日期:2023-12-11

一、项目简介

本项目主要基于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
      

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