基于面向对象的思想来使用结构体,将会有意想不到的效果

程序开发过程中,很多人都会接触到客户服务端模型,通常客户服务端模型是基于socket的网络通信,而网络通信是需要定义通信协议,通信协议结构一般是用结构体的方式来表示,而数据内容有的可能会使用json格式,对于嵌入式设备,数据内容更多的还是采用结构体的方式来表示。

本文首先会基于Qt提供的socket接口来实现一个简单的客户服务端模型,主要是为后面数据内容采用结构体通信的说明提供基础。接着定义通信协议结构体,然后再说明C语言方式使用结构体的方法,再介绍基于面向对象的思想来使用结构体,从而体会两者方式之间的区别,最后再介绍如何采用模版方式来更好的获取结构格式不定的数据内容。

一、客户服务端模式

客户服务端模式的机制是,服务端启动监听端口来等待客户端的连接,客户端创建socket启动连接,服务端成功接收到连接之后,等待客户端发送数据,客户端开始发送数据,服务端接收到数据,并进行解析处理。

下面会基于QT提供的socket接口来实现简单的客户服务端模型,实现之前需要在pro文件中添加network库的支持。

QT       += core gui network

1、定义实现简单的服务端类JTcpServer, 首先构造函数创建QTcpServer对象用来启动监听等待新的连接,当有新的连接请求的时候,则通过QTcpServer提供的接口nextPendingConnection来返回连接成功的socket, 然后等待客户端发送数据,如果有可读数据,那么读取数据进行处理。

// 定义服务端类
class JTcpServer : public QObject
{
    Q_OBJECT
public:
    JTcpServer();
    ~JTcpServer();

    void Start();

public slots:
    void AcceptConnection();
    void ReadClient();

private:
    QTcpServer* m_pTcpServer;
    QTcpSocket *m_pClientConnection;
};

// 实现服务端类
JTcpServer::JTcpServer()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pTcpServer = new QTcpServer(this);
}

JTcpServer::~JTcpServer()
{
    LOG(INFO) << " decontructor";
    m_pTcpServer->close();
}

void JTcpServer::AcceptConnection()
{
    LOG(INFO) << "receive new connection";
    m_pClientConnection = m_pTcpServer->nextPendingConnection();
    if (m_pClientConnection->waitForReadyRead())
    {
        ReadClient();
    }
}

void JTcpServer::ReadClient()
{
    QString str = m_pClientConnection->readAll();
    LOG(INFO) << "str: " << str.toStdString().c_str();
}

void JTcpServer::Start()
{
    LOG(INFO) << "start tcp server";

    m_pTcpServer->listen(QHostAddress::Any, 9999);
    if (m_pTcpServer->waitForNewConnection(500000))
    {
        AcceptConnection();
    }

    LOG(INFO) << "end tcp server";
}

2、定义实现简单的客户端类,构造函数创建QTcpSocket用来连接服务端,并且发送数据。

// 定义客户端类
class JTcpClient : public QObject
{
    Q_OBJECT
public:
    JTcpClient();
    ~JTcpClient();

    void Start();

private:
    QTcpSocket* m_pclientSocket;
};

// 实现服务端类
JTcpClient::JTcpClient()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pclientSocket = new QTcpSocket(this);
}

JTcpClient::~JTcpClient()
{
    LOG(INFO) << " decontructor";
    m_pclientSocket->close();
}

void JTcpClient::Start()
{
    LOG(INFO) << "start tcp client";

    m_pclientSocket->connectToHost(QHostAddress("127.0.0.1"), 9999);
    char ac_data[512] = {0};
    std::memcpy(ac_data, "hello everyone!", sizeof("hello everyone!"));
    m_pclientSocket->write(ac_data);
    m_pclientSocket->waitForBytesWritten();

    LOG(INFO) << "end tcp client";
}

3、完成客户端和服务端的实现,启动两个分离线程来分别执行客户端和服务端代码

// 启动服务端
std::thread thread_server( [&]{
    JTcpServer *p_tcp_server = new JTcpServer();
    p_tcp_server->Start();
} ) ;

std::this_thread::sleep_for(std::chrono::seconds(1));

// 启动客户端
std::thread thread_client( [&]{
    JTcpClient *p_tcp_client = new JTcpClient();
    p_tcp_client->Start();
} ) ;

if (thread_server.joinable())
{
    thread_server.detach();
}

if (thread_client.joinable())
{
    thread_client.detach();
}

4、启动运行之后,可以看到服务端成功打印了客户端发送的数据,这说明客户端和服务端之间是能够通信的。

[void JTcpServer::ReadClient():60] str: hello everyone!

二、通信协议

定义客户端和服务端的通信协议,它包括帧号,该帧号具有唯一性;帧类型,根据具体业务场景进行定义,比如命令帧、结果帧等;帧的来源表示帧的发送者; 帧的目的表示帧的接受者;数据帧长度则存储数据内容的具体长度; 数据则存放不定长度的数据内容。

三、基于C语言方式的结构体

基于C语言方式定义通信协议的结构体

typedef struct CFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
}CFRAME;

再定义数据内容的结构体

typedef struct CParam
{
    int iParam;
    char acInfo[32];
}CPARAM;

客户端构建数据,并发送。首先malloc申请内存,然后填充帧头和数据内容,最后发送数据,再free释放内存。

CFRAME *p_frame = nullptr;
p_frame = (CFRAME *)malloc(sizeof(CFRAME) + sizeof(CPARAM));
memset(p_frame, 0x00, sizeof(CFRAME) + sizeof(CPARAM));
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(CPARAM);
CPARAM param;
param.iParam = 400;
memcpy(param.acInfo, "happy.", sizeof("happy."));
memcpy(p_frame->data,  &param, sizeof(CPARAM));

int i_write_len = m_pclientSocket->write((char *)p_frame, sizeof(CFRAME) + sizeof(CPARAM));
LOG(INFO) << "i_write_len: " << i_write_len;
m_pclientSocket->waitForBytesWritten();

free(p_frame);

服务端接收数据,并解析。服务端接收全部数据,然后解析并打印出来。由于TCP是流的方式,一般来说,先解析帧头,再解析帧数据,但是这里暂时不需要关注,所以没有考虑。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CFRAME *p_frame = reinterpret_cast<CFRAME *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

CPARAM *p_param = reinterpret_cast<CPARAM *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

四、基于面向对象的结构体

上面的方式是基于基于C语言的方式来使用结构体,接下来就来说明如何基于面向对象的方式来使用结构体。

基于面向对象方式定义通信协议的结构体

struct CXXFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
    CXXFrame()
        : iId(1)
        , iType(0)
        , iFrom(1)
        , iTo(2)
        , iDataLen(0)
    {}
};

基于面向对象方式定义数据内容结构体

struct JParam
{
    int iParam;
    char acInfo[32];
    JParam()
        : iParam(1)
    {
        memset(acInfo, 0x00, sizeof(acInfo));
    }
};

构建客户端数据,并且发送数据。为了避免申请内存而忘记释放,这里使用std::vector来定义数组来存储发送的数据,这样就避免忘记释放内存。

CXXFrame *p_frame = nullptr;
int i_frame_len = sizeof(CXXFrame) + sizeof(JParam);
std::vector<char> vec_frame(i_frame_len, 0);
p_frame =  reinterpret_cast<CXXFrame *>(vec_frame.data());
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(JParam);
JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
p_param->iParam = 400;
memcpy(p_param->acInfo, "happy!", sizeof("happy!"));
m_pclientSocket->write(vec_frame.data(), vec_frame.size());
m_pclientSocket->waitForBytesWritten();

服务端接收数据,并且解析数据。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

五、模版方式获取数据内容

服务器接收数据的内容的长度是不确定,不同的应用场景对应不同的结构体。为了统一、并且简化代码量。可以实现模版方法来获取数据内容。

template<typename T>
T* GetFrameParam(CXXFrame *pFrame)
{
    return reinterpret_cast<T*>(pFrame->data);
}

那么服务端接收数据内容,就可以调用模版方法。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

//JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
JParam *p_param = GetFrameParam<JParam>(p_frame);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

六、总结

本文通过实现一个简单的客户服务端模型来说明C语言定义的结构体和面向对象定义的结构体的区别,通过面向对象定义的结构体可以在构造函数中初始化数据成员,这样就不必每次定义对象,还需要使用memset来初始化结构体,同时也避免了忘记初始化结构体的可能性。另外一个,我觉得可以很好简化代码量,并且让代码统一的一个点就是,使用模版方法来解析帧的数据内容。除了解析帧的数据内容之外,还可以通过模版方法来构建帧的数据内容,这样不同结构体,只需要一个模版方法就可以解决。所以,使用面向对象的设计语言的时候,尽量用面向对象的设计思想来开发,很多时候可以简化代码,甚至简化代码的逻辑。

发表评论

电子邮件地址不会被公开。 必填项已用*标注