欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

使用Qt进行基于TCP的socket通信(类似于IO多路复用)

最编程 2024-01-26 11:40:31
...

这里我们不利用多线程实现服务器和客户端的通信,基于套接字的复用操作,实现服务器与多客户端的通信,同时为后续的客户端与客户端通信铺设基础结构。

在介绍代码结构之前需要熟悉Qt TCP通信需要的一些类和函数。

主要的类

QTcpSocket

1、QTcpSocket 类提供一个TCP套接字。
2、TCP是一个面向连接,可靠的的通信协议,非常适合于连续不断的数据传递。
3、QTcpSocket 是QAbstractSocket类非常方便的一个子类,让你创建一个TCP连接和数据流交流。

注意:TCP套接字不能以QIODevice::Unbuffered模式来打开

QTcpServer

1、QTcpServer提供一个TCP基础服务类 继承自QObject,这个类用来接收到来的TCP连接,可以指定TCP端口或者用QTcpServer自己挑选一个端口,可以监听一个指定的地址或者所有的机器地址。
2、其调用listen()来监听所有的连接,每当一个新的客户端连接到服务端就会发射信号newConnection() ,调用nextPendingConnection()来接受待处理的连接。返回一个连接的QTcpSocket(),我们可以用这个返回的套接字和客户端进行连接。
3、如果有错误,serverError()返回错误的类型。调用errorString()来把错误打印出来。
4、当监听连接时候,可以调用serverAddress()和serverPort()来返回服务端的地址和端口。 
5、调用close()来关闭套接字,停止对连接的监听。‘
6、尽管QTcpServer大多时候设计使用事件循环,也可以不适用事件循环,可以使用waitForNewConnection(),会一直阻塞,知道一个连接可以用或者超时。

主要函数

1、incomingConnection
void QTcpServer::incomingConnection(qintptr socketDescriptor);

当QTcpServer有一个新连接时候调用这个虚函数,socketDescriptor参数是新连接的套接字描述符。这个函数新建一个QTcpSocket套接字,建立套接字描述符,然后存储套接字在一个整型的待连接链表中。最后发射信号newConnection()。重写这个函数,当一个新连接时候,来调整这个函数的行为。当服务端使用QNetworkProxy服务器代理时候,使用一般的套接字函数套接字描述符可能不可以用,这时候应该使用QTcpSocket::setSocketDescriptor()来设置描述符。所以incomingConnection一般与setSocketDescriptor形影不离。

2、setSocketDescriptor
virtual bool setSocketDescriptor(qintptr socketDescriptor, SocketState state = ConnectedState,OpenMode openMode = ReadWrite);

用本机套接字描述符socket descriptor初始化qabstractsocket。如果接受socket descriptor作为有效的套接字描述符,则返回true;否则返回false。套接字以openmode指定的模式打开,并进入socket state指定的套接字状态。读写缓冲区被清除,丢弃所有挂起的数据。

3、readyRead
void readyRead();

每次有新数据可供从设备读取时,此信号都会发出一次。只有当新数据可用时才会再次发出,例如当网络数据的新有效负载到达您的网络套接字时,或者当新的数据块已附加到您的设备时。

readyRead()不会递归发出;如果您重新进入事件循环或在连接到readyRead()信号的插槽内调用waitForReadyRead(),则不会重新发送信号(尽管waitForReadyRead()可能仍然返回true)。

对于实现从qiodevice派生的类的开发人员,请注意:当新数据到达时,应始终发出readyread()(不要只发出它,因为缓冲区中还有数据要读取)。在其他情况下不要发出readyread()。

4、connectToHost
virtual void connectToHost(const QString &hostName, quint16 port, OpenMode mode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);

尝试连接到给定端口上的主机名。协议参数可用于指定要使用的网络协议(如IPv4或IPv6)。

套接字在给定的openmode中打开,首先进入hostLookupState,然后执行主机名查找。如果查找成功,将发出hostfound(),QabstractSocket将进入ConnectingState。然后它尝试连接到查找返回的一个或多个地址。最后,如果建立了连接,qAbstractSocket将进入ConnectedState并发出Connected()。

在任何时候,套接字都可以发出error()来表示发生了错误。

主机名可以是字符串形式的IP地址(如“43.195.83.32”),也可以是主机名(如“example.com”)。QabstractSocket仅在需要时执行查找。端口按本机字节顺序排列。

5、waitForConnected
virtual bool waitForConnected(int msecs = 30000);

等待套接字连接,最长为毫秒。如果连接已建立,则此函数返回true;否则返回false。在返回false的情况下,可以调用error()来确定错误的原因。

以下示例最多等待一秒钟以建立连接:

socket->connecttohost(“imap”,143);
if(socket->waitforconnected(1000))。
    qDebug() << "已经连接";

如果msec为-1,则此函数不会超时。

6、disconnectFromHost
virtual void disconnectFromHost();

尝试关闭套接字。如果有等待写入的挂起数据,QabstractSocket将进入关闭状态,并等待所有数据写入。最终,它将进入未连接状态并发出disconnected()信号。

7、errorString
QString errorString() const;

返回上一个错误的可读描述。

8、listen
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);

通知服务器侦听地址地址和端口上的传入连接。如果端口为0,则自动选择端口。如果地址为qhostaddress::any,服务器将监听所有网络接口。成功时返回true;否则返回false。


代码结构

image.png

其中包括:
1、服务器和客户端的界面
2、基于QTcpSocket的通信类
3、基于QTcpServer的服务器类
4、保证服务器单一对象使用的Common类

服务器界面


image.png

客户端界面


image.png

为了便于发送的消息数据的类型区分,这里我们引入发送接收数据的特定格式,既可以简单的加密,也可以让消息区分更加容易。

例如下面两段针对格式的组合和解析:
发送函数部分实现代码:

//发送消息,消息内容存放在QMap中
void TcpClientSocket::sendMessage(QMap<QString,QString> message)
{
。。。
    //把消息按照 [消息名称:消息内容]  的格式进行存储
    foreach( QString key,message.keys())
    {
        QString tempMsg = key + ":" +message[key];
        out << tempMsg;
    }

。。。
}

接收函数部分实现代码:

//递归方式接收消息,当接收的消息满足需要的size后,发送信号。
void TcpClientSocket::receiveMessage()
{
。。。
    //读取信息并按照 [消息名称:消息内容] 格式进行解析
    QMap<QString,QString> message;
    int msgSize = 0;
    while( _blockSize > msgSize)
    {
        QString tempMsg;
        in >> tempMsg;
        int idxSplitor = tempMsg.indexOf(":");
        message[tempMsg.mid(0,idxSplitor)] =
                tempMsg.mid(idxSplitor+1,tempMsg.length() - idxSplitor -1);
        msgSize = initBytes - bytesAvailable();
    }
。。。
}

套接字的复用操作
需要重写incomingConnection函数(这也是之后我们实现客户端与客户端通信的基础),实现的部分代码如下:

void TcpServer::incomingConnection ( int socketDescriptor )
{
    TcpClientSocket *tcpClientSocket = new TcpClientSocket(this);
    tcpClientSocket->setSocketDescriptor(socketDescriptor);
    Server* s =Common::getServerInstance();
    s->addClient(tcpClientSocket);

。。。
}

运行效果

服务器与两个客户端的通信效果:


image.png
image.png
image.png

具体实现代码:

推荐阅读