• IMLite轻量级即时通信工具开发指南


    花了一周时间开发了一个简单的即时通信工具,勉强算是程序原型。现在我把开发流程和一些个人的想法记录下来。本文首先介绍程序架构和通信接口,之后会聚焦到服务器的信号槽设计原则,接下来将解释有关TCP通信的粘包问题和解决方案,最后一个部分是一些改进建议。

    源码下载:https://gitee.com/learnhow/imlite

    程序架构图例:

    一、架构方案介绍

    主程序(MainWindow)启动服务器(TcpServer),在收到客户端的连接请求之后会开启一个线程并创建子连接(TcpSocket)。目前支持的消息类型有三种,分别为:Message——由客户端发起的端到端消息或直接由服务器发起的广播消息,这是即时通信最基础的通信要求。MessageConnCallback——由服务器向新连接的终端发起的socketID反向注册信息,目的是告知客户端本次连接在服务器端生成的socketID。MessageConnecter——由服务器向所有终端发布的一条广播,通知所有终端其他的终端socketID和客户端自定义的nickname,这是实现端到端通信的基础。

    端到端通信的原理实际上是由客户端发送一条携带了接收方socketID的信息。服务器在收到后会解析并进行转发。

    为了让多种消息类型能够统一,程序提供了MessageInterface接口和TcpSocketData对象。所有的消息类型都必须实现MessageInterface接口,并且在发送端和接收端都必须通过TcpSocketData来注册消息类型包括进行序列化和反序列化。

    NetPacket提供了消息打包和解包方法。为发送的数据包加上一个字符串作为包尾,并且在接收端可以根据包尾来解包并分割为原始报文。更详细的信息将在第三节介绍。

    SQLiteService实现了一种简单的数据保存方法,由于程序开发的重点不在于此,并没有做专门设计。二次开发中应该会重新设计,这里一带而过。

    二、通信机制介绍

    本例中所有对象的通信都采用信号槽,信号槽是Qt提供的一种消息流转机制。在命名规则上,程序按照Qt的命名方案:槽的命名使用动词一般现在时,信号命名使用动词过去时。并且以“谁创建谁连接”的原则编写,例如:TcpServer负责创建所有的子连接TcpSocket,因此与它的信号槽连接都会放在TcpServer中实现。

    接下来以客户端向服务器端发起连接请求为例做简单介绍。服务器(TcpServer)通过incomingConnection(qintptr)方法产生socketID,立刻通过子连接向终端发送connectionCallback信号,接着会发送广播向终端通知有新的连接加入,最后还会向UI层发送一个与显示有关的信号(程序中表现为在QWidgetList中增加一个Item)。

    // 当有新的终端连接请求时被调用
    void TcpServer::incomingConnection(qintptr handle)
    {
        ...
    
        clientStatusConnect(QString("%1").arg(handle));
    }
    
    void TcpServer::clientStatusConnect(QString socketDescriptor)
    {
    
        socketnicknamemap.insert(socketDescriptor, "Guest");
        // 向终端反向注册handle
        MessageConnCallback callback; 
        callback.setSocketDescriptor(socketDescriptor);
        emit connectionCallback(callback); ①
    
        MessageConnecter msgConn;
        msgConn.setConnectors(socketnicknamemap);
        emit terminalsPublished(msgConn); ②
    
        // 反馈信号至UI:增加终端
        emit clientStatusTriggered(socketDescriptor, 1);
    }

    其它的代码就不一一解释了。

    三、Tcp粘包和解决方案

    观察上面的源码片段,当有新连接产生后,服务器会“同时”向终端发送两条数据(注:①②)。客户端的void dataRecv()方法会调用QByteArray buff = tcpSocket->readAll()读取数据,这时有极大的几率发生“粘包”——服务器发送的两条报文A、B,客户端以A+B作为一条报文读取出来。我的解决方案是在发送端为每段报文增加一个字符串(CF07D)作为结束标志,在接收端就以此标志对报文分割。

    QByteArray NetPacket::package(const QByteArray &data)
    {
        QByteArray wrap(data);
        wrap += splite;
        return wrap;
    }
    
    QList<QByteArray> NetPacket::unpackage(const QByteArray &wrap)
    {
        QList<QByteArray> bufflist;
        int pos = 0;
        int prev = 0;
        while((pos = wrap.indexOf(splite, pos)) != -1) {
            QByteArray part = wrap.mid(prev, pos);
            bufflist.push_back(part);
            pos += splite.length();
            prev = pos;
        }
        return bufflist;
    }

    四、问题及改进建议

    (1)信号槽是一个好东西,但是不能滥用:程序中所有对象的数据流转都是通过信号槽机制。不可否认这样做似乎减少了模块耦合,但是增加了维护的难度。并且看上去也显得“乱糟糟”的。如果一条数据同时需要经过多个对象处理,就必须为此设计多个信号槽。

    (2)把消息分开传输:服务器的自连接与客户端之间只使用了一个socket端口通信(8361),这样在接收端就必须为不同的消息类型设计不同的type值。更加合理的做法是让客户端与服务器同时使用多个端口进行通信,既有利于需求扩展又不至于让接收端的方法无限膨胀。

  • 相关阅读:
    kettle参数、变量详细讲解[转]
    C# 异步
    〖Python〗-- 模块与包
    〖Python〗-- 异常处理
    〖Python〗-- 面向对象进阶
    〖Python〗-- 反射、内置attr、包装
    〖Python〗-- property、静态方法、类方法
    〖Python〗-- 面向对象编程的继承、多态与多态性、封装
    〖Python〗-- 面向对象编程、继承、组合、接口和抽象类
    〖Python〗-- 递归、面向对象初识及编程思想
  • 原文地址:https://www.cnblogs.com/learnhow/p/8641965.html
Copyright © 2020-2023  润新知