编写C++服务器时报文格式不要使用继承关系

两种关系示例

在使用C++ socket编写服务器的时候,我们会使用struct指定一系列结构体作为消息报文,一般来说这些结构体都会有相同的首部,并在首部包含该结构体类型的信息。关于首部和其他消息的关系,我们一般可以选择继承关系和复合关系两种。示例如下:

  1. 继承关系
    struct Msg {
    	Msg(CMD cmd = CMD::LOG_ERROR, int len = sizeof(Msg))
    		:_cmd(cmd), _len(len) {}
    	virtual ~Msg() {}
    	CMD _cmd;
    	int _len;
    };
    	
    struct MsgLogin : public Msg {
    	MsgLogin()
    		: Msg(CMD::LOGIN, sizeof(MsgLogin))
    		, _username(""), _password("") {}
    	virtual ~MsgLogin() {};
    	char _username[NAME_LEN];
    	char _password[PASSWD_LEN];
    };
    
  2. 复合关系
    struct MsgHeader {
    	MsgHeader(CMD cmd = CMD::LOG_ERROR, int len = sizeof(MsgHeader))
    		:_cmd(cmd), _len(len) {}
    	CMD _cmd;
    	int _len;
    };
    
    struct MsgLogin {
    	MsgLogin()
    		: _header(CMD::LOGIN, sizeof(MsgLogin))
    		, _username(""), _password("") {}
    	MsgHeader _header;
    	char _username[NAME_LEN];
    	char _password[PASSWD_LEN];
    };
    

对比分析

在bilibili上刘远东老师的C++高并发服务器教程中,老师使用的是继承关系。然而我认为,无论在什么情况下都更应使用复合关系。继承关系的缺点在于析构函数上:

  1. 如果不使用虚析构函数,那么当一个Msg类的指针指向MsgLogin对象时,如果对该指针使用delete函数,则无法调用MsgLogin的析构函数,造成内存泄露。

    Msg* msg = new MsgLogin;
    delete msg; /*内存泄漏*/
    
  2. 如果使用虚析构函数,则这一系列类就是包含虚函数的类,编译器会为他们创建虚函数表。这时候类的内存结构会发生变化。以下是一个Msg的内存结构。
    在这里插入图片描述

    在这种情况下,我们无法简单地通过

    Msg msg;
    recv(socket, (char*)&msg, sizeof(msg), 0);
    

    来将接收到的信息写入到Msg实例中。而是需要仔细计算一个指针的偏移量来接收信息。

    Msg msg;
    recv(socket, (char*)&msg+sizeof(void*), sizeof(msg)-sizeof(void*), 0)
    

    这样首先可读性不强,在这里看到个sizeof(void*)一般人的反应都是一脸懵逼,其次就是风险大。忘写虚析构和忘写偏移量都会导致致命的错误。

对于复合关系,相对于C语言的struct来说,在增加了构造函数提供的便利的同时,避免了析构函数引起的繁琐的问题。所以对于网络报文格式的定义,复合关系是比继承关系更合适的一种设计。

猜你喜欢

转载自blog.csdn.net/weixin_43558951/article/details/107769435
今日推荐