这里我们不利用多线程实现服务器和客户端的通信,基于套接字的复用操作,实现服务器与多客户端的通信,同时为后续的客户端与客户端通信铺设基础结构。
在介绍代码结构之前需要熟悉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。
代码结构

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

客户端界面

为了便于发送的消息数据的类型区分,这里我们引入发送接收数据的特定格式,既可以简单的加密,也可以让消息区分更加容易。
例如下面两段针对格式的组合和解析:
发送函数部分实现代码:
//发送消息,消息内容存放在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);
。。。
}
运行效果
服务器与两个客户端的通信效果:



具体实现代码:
网友评论