CodeReview(ClusterChatServer)

2024/11/20

ClusterChatServer为本人大四简历上面的一个项目,实现的是一个简单的集群聊天服务器,其中支持一对一以及多对多的聊天功能!如果你对该项目感兴趣的话,可以在GitHub上进行查看。

1. 存在的问题总结

1.1 可见域问题

该问题可能是工作之前很少注意到吧!记得刚开始工作的时候也遇到过一些这样的问题,当时是被同事纠正过来!

第一处:

class ChatService : public SingletonTemplate<ChatService> {
public
	void Login(const TcpConnectionPtr &, json &, Timestamp);
  void Register(const TcpConnectionPtr &, json &, Timestamp);
  void OneChat(const TcpConnectionPtr &, json &, Timestamp);
  void GroupChat(const TcpConnectionPtr &, json &, Timestamp);
  void AddFriend(const TcpConnectionPtr &, json &, Timestamp);
  void QueryFriends(const TcpConnectionPtr &, json &, Timestamp);
  void CreateGroup(const TcpConnectionPtr &, json &, Timestamp);
  void DeleteGroup(const TcpConnectionPtr &, json &, Timestamp);
  void JoinGroup(const TcpConnectionPtr &, json &, Timestamp);
  void QuitGroup(const TcpConnectionPtr &, json &, Timestamp);
  void UpdateUser(const TcpConnectionPtr &, json &, Timestamp);
  // 对外暴露的接口只有这两个
  MessageHandler getHandler(MessageType type);
  void clientCloseException(const TcpConnectionPtr &connection);
private
  std::unordered_map<MessageType, MessageHandler> _msgHandlerMap;
}

这些接口外部并不是直接使用这些借口的,而是通过getHandler来访问的,所以这些接口的可见域应该为private

1.2 接口实现过于过程化

void ChatService::Login(const TcpConnectionPtr &connection, json &data,
                        Timestamp time) {
  LOG_INFO << "Checking Login...";
  int id = data["id"];
  std::string pwd = data["password"];

  User user = _userModel.query(id);

  json response;
  response["msgtype"] = MessageType::LOGIN_MSG_ACK;
  // LOG_INFO << "Checking user's password...";
  if (user.getId() != -1 && user.getPwd() == pwd) {
    if (user.getState() == "online") {
      // the user is already online
      LOG_INFO << "Login failed, the user is already online!";
      response["status"] = 401;
      response["errorMessage"] = "The user already login!";
    } else {
      // login successfully and store the user's connection
      // !!!need to consider the concurrency problem
      {
        std::lock_guard<std::mutex> lock(_mutex);
        _userConnMap.insert({id, connection});
        _userMap.insert({connection, id});
      }
      // begin to send the message to the user
      LOG_INFO << "Login successful!";
      user.setState("online");
      _userModel.update(user);
      response["status"] = 200;
      response["id"] = user.getId();
      response["name"] = user.getName();

      // 1. get the offline message from the database
      std::vector<Message> old_message = _messageModel.queryMsg(user.getId());
      if (!old_message.empty()) {
        response["offlinemsg"] = old_message;
      }
      // delete all the offline message, because the user has already read them
      _messageModel.deleteMsg(old_message);
      // 2. get the user's all friends
      std::vector<User> friends = _friendModel.query(user.getId());
      if (!friends.empty()) {
        response["friends"] = friends;
      }
      // 3. get the user's all groups
      std::vector<Group> groups = _groupModel.queryGroups(user.getId());
      if (!groups.empty()) {
        response["groups"] = groups;
        // get the user's all groups' messages
        for (const auto &group : groups) {
          std::vector<Message> old_group_message =
              _messageModel.queryGroupMsg(group.getId(), user.getId());
          if (!old_group_message.empty()) {
            response["groupmsg"].push_back(old_group_message);
            _messageModel.deleteGroupMsg(old_group_message);
          }
        }
      }
    }
  } else {
    LOG_INFO << "Login failed, the password is Wrong!";
    response["status"] = 400;
    response["errorMessage"] =
        "The password is wrong or the user is not exist!";
  }
  // std::cout << std::setw(4) << response;
  connection->send(response.dump());
}

你能想象这是一个登陆接口的实现吗?这里完全可以拆分为几个函数来实现,否则就会出现在公司当初见到的那个近千行的函数实现!

这里当时我在公司还吐槽当时实现的人怎么会写出这样的代码,现在一看当初的我也是这么干的,就这么将整个登陆路程讲述了一遍!

对上面的代码简化后:

void ChatService::Login(const TcpConnectionPtr &connection, json &data, Timestamp time) {
    LOG_INFO << "Checking Login...";

    // Parse user credentials
    int id = data["id"];
    std::string pwd = data["password"];
    json response;
    response["msgtype"] = MessageType::LOGIN_MSG_ACK;

    // Query user from database
    User user = _userModel.query(id);
    if (!ValidateUser(user, pwd, response)) {
        connection->send(response.dump());
        return;
    }

    // Check if user is already online
    if (user.getState() == "online") {
        HandleAlreadyOnline(response);
        connection->send(response.dump());
        return;
    }

    // Handle successful login
    HandleSuccessfulLogin(user, connection, response);

    connection->send(response.dump());
}

// 验证用户
bool ChatService::ValidateUser(const User &user, const std::string &pwd, json &response) {
    if (user.getId() == -1 || user.getPwd() != pwd) {
        LOG_INFO << "Login failed, the password is Wrong!";
        response["status"] = 400;
        response["errorMessage"] = "The password is wrong or the user does not exist!";
        return false;
    }
    return true;
}

// 处理用户已经在线的情况
void ChatService::HandleAlreadyOnline(json &response) {
    LOG_INFO << "Login failed, the user is already online!";
    response["status"] = 401;
    response["errorMessage"] = "The user is already logged in!";
}

// 处理登录成功的逻辑
void ChatService::HandleSuccessfulLogin(User &user, const TcpConnectionPtr &connection, json &response) {
    LOG_INFO << "Login successful!";

    // Update user state and store connection
    {
        std::lock_guard<std::mutex> lock(_mutex);
        _userConnMap[user.getId()] = connection;
        _userMap[connection] = user.getId();
    }

    user.setState("online");
    _userModel.update(user);

    response["status"] = 200;
    response["id"] = user.getId();
    response["name"] = user.getName();

    // Populate user data
    PopulateOfflineMessages(user, response);
    PopulateFriendList(user, response);
    PopulateGroupData(user, response);
}

// 获取用户的离线消息
void ChatService::PopulateOfflineMessages(const User &user, json &response) {
    std::vector<Message> oldMessages = _messageModel.queryMsg(user.getId());
    if (!oldMessages.empty()) {
        response["offlinemsg"] = oldMessages;
        _messageModel.deleteMsg(oldMessages);
    }
}

// 获取用户的好友列表
void ChatService::PopulateFriendList(const User &user, json &response) {
    std::vector<User> friends = _friendModel.query(user.getId());
    if (!friends.empty()) {
        response["friends"] = friends;
    }
}

// 获取用户的群组数据及群组消息
void ChatService::PopulateGroupData(const User &user, json &response) {
    std::vector<Group> groups = _groupModel.queryGroups(user.getId());
    if (!groups.empty()) {
        response["groups"] = groups;
        for (const auto &group : groups) {
            std::vector<Message> groupMessages = _messageModel.queryGroupMsg(group.getId(), user.getId());
            if (!groupMessages.empty()) {
                response["groupmsg"].push_back(groupMessages);
                _messageModel.deleteGroupMsg(groupMessages);
            }
        }
    }
}

虽然看着比原来的代码更长了一些,但是逻辑清晰了不止一星半点!

2. 有趣的写法

这里说来惭愧,因为工作之后经常使用的编程语言是Delphi,自己对于C++的学习便一直处于停滞不前的状态,所以本次CodeReview的时候很多写法我都看不懂!但是看懂之后,C++不愧是C++,感觉就是和其他的语言不一样!

2.1 单例模式和友元类

template <typename T> class SingletonTemplate {
public:
  static T &getInstance() {
    static T instance;
    return instance;
  }
  virtual ~SingletonTemplate() = default;
  // delete copy constructor and assignment operator
  SingletonTemplate(const SingletonTemplate &) = delete;
  SingletonTemplate &operator=(const SingletonTemplate &) = delete;
protected:
  SingletonTemplate() = default;
};

class ChatService : public SingletonTemplate<ChatService> {
  // friend class of SingletonTemplate, and SingletonTemplate<ChatService> can
  // access the private members of ChatService
  friend class SingletonTemplate<ChatService>; 
}

这里结合泛型模版以及友元机制来使得单例类的实现十分的简洁!

其中单例模版类,其中将构造函数声明为protected,不能被外界所使用,并且将拷贝构造函数以及赋值函数删除,以及通过提供一个静态的获取单例的函数来实现单例模式。其中由于static的一些特性,保证了一定只构建一个单例。而且与此同时利用C++11以及以后标准当中对于静态变量初始化的线程安全保证。

C++11 标准开始,对函数内部的静态局部变量(如 static T instance)的初始化提供了以下保证:

回顾这段代码的时候,唯一令我困惑的是为什么这里需要将SingletonTemplate<ChatService类声明为ChatService的友元类?原来是这样可以保证类SingletonTemplate<ChatService>可以访问被隐藏的ChatService的构造函数。

2.2 C++当中打印枚举类型值

enum class MessageType {
  // ...
}

std::ostream &operator<<(std::ostream &out, const MessageType type) {
  static const auto strings = [] {
    std::map<MessageType, std::string_view> result;
#define INSERT_ELEMENT(p) result.emplace(p, #p);
    INSERT_ELEMENT(MessageType::LOGIN_MSG);
    INSERT_ELEMENT(MessageType::REGIST_MSG);
#undef INSERT_ELEMENT
    return result;
  };
  return out << strings()[type];
}

这里主要的困惑之处在于result.emplace(p, #p)这里的作用。

宏的作用:简化代码,将枚举值和其字符串表示添加到 result 中。