文章目录
前言
本周我组继续进行黄金点游戏联网化的学习与编写。本次主要的进展有三大部分,一是Server文件的编写,二是编写客户端与服务器的接口文件,三是客户端的编写。
一、Server类的编写
1.类功能简介(.h文件)
首先是代码:
#include <QTcpServer>
#include <QObject>
#include "tcpclientsocket.h" //包含TCP套接字
#include <QQueue>
class Server : public QTcpServer
{
Q_OBJECT //添加宏(Q_OBJECT)是为了实现信号与槽的通信
public:
Server(QObject *parent=0,int port=0);
QList<TcpClientSocket*> tcpClientSocketList;
QQueue<TcpClientSocket*> queue;
signals:
void updateClients();
void showCurrentConnection();
public slots:
void updateServer(TcpClientSocket*,QString,int);
void disconnected(int);
void incomingConnection();
};
其中包含一个构造函数,构造函数将在构建服务器时确定本次使用的端口,由于端口号尽量规避well-known port,我们选用初始的为9527(喜欢星爷的电影)。
其次为了处理客户端的用户加入与退出、客户端传入数据的处理。分别声明QList和QQueue进行处理,其中的变量类型为scocket对象。
该类包含两个信号和三个槽函数,其功能可以从命名清楚地看出。特别指出,由于存在客户端操作和加入等不同步情况,所有的信息都需暂时存储在服务器端。所以updataServer函数便意义非凡。
2.具体实现
首先是构造函数代码:
Server::Server(QObject *parent,int port)
:QTcpServer(parent)
{
listen(QHostAddress::Any,port);
connect(this,SIGNAL(newConnection()),this,SLOT(incomingConnection()));
}
(1)在构造函数中设置对选定端口的监听。同时将连接信号与连接槽函数进行连接。当服务器被构造后就可以准备开始连入操作了。
(2)然后是接入操作:
void Server::incomingConnection()
{
QTcpSocket *clientSocket=this->nextPendingConnection();
TcpClientSocket *tcpClientSocket=new TcpClientSocket(this,clientSocket);
connect(tcpClientSocket,SIGNAL(updateServer(TcpClientSocket*,QString,int)),this,SLOT(updateServer(TcpClientSocket*,QString,int)));
connect(tcpClientSocket,SIGNAL(disconnected(int)),this,SLOT(disconnected(int)));
qDebug()<<tcpClientSocket->clientSocket->socketDescriptor();
tcpClientSocketList.append(tcpClientSocket);
qDebug()<<tcpClientSocketList.count();
emit showCurrentConnection();
}
连接操作是当新连接信号发出时进行的操作。该操作内容简单,其主要功能就是获取客户端的套接号,并且将该信息写入一个新的对象之后存入List之中。
当连接后,就可以激活断开连接操作。同时,新的客户端连入需要引起服务器的信息更新,因此也一起激活(连接)服务器更新槽函数。
(3)然后是服务器更新函数:
void Server::updateServer(TcpClientSocket *clientSocjet, QString msg, int length)
{
for(int i=0;i<queue.size();++i)
if(queue.at(i)==clientSocjet)
return;
clientSocjet->number=msg.left(length).toDouble();
queue.push_back(clientSocjet);
emit updateClients();
}
该函数是将新的数据(即将使用的数据)放入队列。首先同样的客户端连入可以不进行操作。若是还未放入队列的对象,则设置其属性为客户端传入的数值,并将其加入队列。
(4)最后是断开连接:
void Server::disconnected(int descriptor)
{
for(int i=0;i<tcpClientSocketList.count();++i)
{
TcpClientSocket *item=tcpClientSocketList.at(i);
if(item->clientSocket->socketDescriptor()==descriptor)
{
tcpClientSocketList.removeAt(i);
emit showCurrentConnection();
return;
}
}
}
根据需要断开的客户端的标志,进行遍历匹配。若匹配成功则将其移除出List并且更新服务器的显示。
二、TcpClientSocket类的编写
由于该部分是正在学习,并且有参考网络信息与知识,下面只提出部分我们自己的认知。后续深度学习后在加以详细的描述。
1.类功能简介
由于该部分是正在学习,并且有参考网络信息与知识,下面只提出部分我们自己的认知。后续深度学习后在加以详细的描述。
首先看代码:
#include <QTcpSocket>
#include <QObject>
class TcpClientSocket : public QObject
{
Q_OBJECT //添加(Q_OBJECT)是为了实现信号与槽的通信
public:
TcpClientSocket(QObject *parent=0, QTcpSocket *clientSocket=0);
QTcpSocket *clientSocket;
double number;
int score;
signals:
void updateServer(TcpClientSocket*,QString,int);
void disconnected(int);
protected slots:
void dataReceived();
void disconnected();
};
该类主要是最为一个接口接受客户端的信息并且协助双方进行信息的传递。
其中存在一个构造函数和一个QTcpSocket对象、代表传入数字的number、回传的分数score。构造函数在构造时对每一个客户端进行编号处理。
同时其存在两个信号和两个槽函数。主要介绍一下数据的接收槽函数。
2.具体实现(数据接收槽函数)
首先是代码:
void TcpClientSocket::dataReceived()
{
while(clientSocket->bytesAvailable()>0)
{
int length=clientSocket->bytesAvailable();
char buf[1024];
clientSocket->read(buf,length);
qDebug()<<buf;
QString msg=buf;
QTextCodec *codec=QTextCodec::codecForName("UTF-8");
emit updateServer(this,codec->fromUnicode(msg),length);
}
}
该函数会首先检查缓冲区,若缓冲区存在数据则进行读入。代码中设置的目标数组为buf(缓冲区),但实际上该函数实现的是数据读入服务器内部进行后续操作。读入结束后,发出更新服务器的信号,完成操作。
三、客户端的编写
客户端的书写较之服务器工作量要减少不少,并且由于在书写服务器时对代码进行了学习研究,客户端的代码理解要快速,代码书写要高效。
1.类功能简介
首先来看代码:
#include <QDialog>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QGridLayout>
#include <QHostAddress>
#include <QTcpSocket>
class TcpClient : public QDialog
{
Q_OBJECT
public:
TcpClient(QWidget *parent = 0,Qt::WindowFlags f=0);
~TcpClient();
private:
QLabel *scorePromptLabel;
QLabel *scoreLabel;
QLineEdit *sendLineEdit;
QPushButton *sendBtn;
QLabel *userNameLabel;
QLineEdit *userNameLineEdit;
QLabel *serverIPLabel;
QLineEdit *serverIPLineEdit;
QLabel *portLabel;
QLineEdit *portLineEdit;
QPushButton *enterBtn;
QGridLayout *mainLayout;
bool status;
int port;
QHostAddress *serverIP;
QString userName;
QTcpSocket *tcpSocket;
public slots:
void enter();
void connected();
void disconnected();
void dataReceived();
void send();
};
由于客户端主要是被动的进行活动,所以客户端书写时并没有信号变量,只有五个槽函数,以接收不同的信号来完成各项功能。除去进行ui界面搭建的lable等变量,客户端需要和服务器一致,定义一个port变量,来与服务器确定的端口进行匹配。当然,服务器端与客户端进行通信需要一个接口,客户端同样如此,QTcpSocket对于双方的数据传输和链接至关重要。
2.具体实现
(1)首先是构造函数
TcpClient::TcpClient(QWidget *parent,Qt::WindowFlags f)
: QDialog(parent,f)
{
setWindowTitle(tr("黄金点游戏客户端"));
QPixmap pixmap(QString::fromUtf8(":/new/prefix1/023.jpg"));//
QPalette palette = this->palette();
palette.setBrush(backgroundRole(), QBrush(pixmap));
setPalette(palette);
scorePromptLabel=new QLabel(tr("当前得分:"));
scoreLabel=new QLabel;
sendLineEdit=new QLineEdit;
sendBtn=new QPushButton(tr("发送"));
userNameLabel=new QLabel(tr("姓名:"));
userNameLineEdit=new QLineEdit;
serverIPLabel=new QLabel(tr("服务器地址:"));
serverIPLineEdit=new QLineEdit;
portLabel=new QLabel(tr("端口:"));
portLineEdit=new QLineEdit;
enterBtn=new QPushButton(tr("进入游戏"));
mainLayout=new QGridLayout(this);
mainLayout->addWidget(scorePromptLabel,0,0);
mainLayout->addWidget(scoreLabel,0,1);
mainLayout->addWidget(sendLineEdit,1,0);
mainLayout->addWidget(sendBtn,1,1);
mainLayout->addWidget(userNameLabel,2,0);
mainLayout->addWidget(userNameLineEdit,2,1);
mainLayout->addWidget(serverIPLabel,3,0);
mainLayout->addWidget(serverIPLineEdit,3,1);
mainLayout->addWidget(portLabel,4,0);
mainLayout->addWidget(portLineEdit,4,1);
mainLayout->addWidget(enterBtn,5,0,1,2);
scoreLabel->setText(QString::number(0));
status=false;
port=9090;
portLineEdit->setText(QString::number(port));
serverIP=new QHostAddress();
qDebug()<<serverIP->toString();
connect(enterBtn,SIGNAL(clicked(bool)),this,SLOT(enter()));
connect(sendBtn,SIGNAL(clicked(bool)),this,SLOT(send()));
sendBtn->setEnabled(false);
}
这部分与服务器无异。我们给界面增加了一个背景图,代码主要是用到了QPixmap和QPalette,很简单。在构造时,需要处理好界面三个按钮的状态问题,设置“发送”为无法点击状态,其余两个按钮进行与槽函数的链接。
析构函数简单,直接给出代码
TcpClient::~TcpClient()
{
delete scorePromptLabel;
delete scoreLabel;
delete sendLineEdit;
delete sendBtn;
delete userNameLabel;
delete userNameLineEdit;
delete serverIPLabel;
delete serverIPLineEdit;
delete portLabel;
delete portLineEdit;
delete enterBtn;
delete mainLayout;
delete serverIP;
if(tcpSocket)
delete tcpSocket;
}
(2)然后是进入游戏函数
首先是代码
void TcpClient::enter()
{
if(!status)
{
/* 完成输入合法性检验 */
QString ip=serverIPLineEdit->text();
if(!serverIP->setAddress(ip))
{
QMessageBox::information(this,tr("error"),tr("server ip address error!"));
return;
}
if(userNameLineEdit->text()=="")
{
QMessageBox::information(this,tr("error"),tr("User name error!"));
return;
}
userName=userNameLineEdit->text();
/* 创建了一个QTcpSocket类对象,并将信号/槽连接起来 */
tcpSocket=new QTcpSocket(this);
connect(tcpSocket,SIGNAL(connected()),this,SLOT(connected()));
connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(disconnected()));
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(dataReceived()));
tcpSocket->connectToHost(*serverIP,port);
if(tcpSocket->waitForConnected())
status=true;
}
else
{
tcpSocket->disconnectFromHost();
status=false;
}
}
该部分主要是对ip与端口进行合理性验证,由于需要进行双端连接与数据传输,此时需要一个QTcpSocket对象进行处理。
断开连接非常简单,不需要传回其他信息,代码如下:
void TcpClient::connected()
{
sendBtn->setEnabled(true);
enterBtn->setText(tr("离开游戏"));
}
当然,一般这种书写都是配套的,写习惯了,会很自然的添加
void TcpClient::disconnected()
{
scoreLabel->setText(QString::number(0));
status=false;
sendBtn->setEnabled(false);
enterBtn->setText(tr("进入游戏"));
}
(3)最后一部分是数据的发送和接收
首先是数据的发送
void TcpClient::send()
{
if(sendLineEdit->text()=="")
return;
QTextCodec *codec=QTextCodec::codecForName("UTF-8");
tcpSocket->write(codec->fromUnicode(sendLineEdit->text()),sendLineEdit->text().length());
sendLineEdit->clear();
}
与服务器端类似,只需要一个简单的write函数,注意发送后将界面输入行清空。
然后是分数的接收
void TcpClient::dataReceived()
{
while(tcpSocket->bytesAvailable()>0)
{
QByteArray datagram;
datagram.resize(tcpSocket->bytesAvailable());
tcpSocket->read(datagram.data(),datagram.size());
QString msg=datagram.data();
if(datagram.size()>0)
scoreLabel->setText(msg.left(datagram.size()));
}
}
通过read函数从缓冲区中读出传回的分数数据。注意此时我们用到的是字节流,但由于Qt,对于字节流和字符串的操作有可以直接处理的类,所以也不是大问题。最后,修改界面上的显示。
# 总结 本次结对编程,我们对编写一个基于TCP协议的黄金点游戏进行了更深一步 的学习。虽然,我们更多的是先进行检索,读懂后进行编程,但通过这种学习方式我们切实的学会了实用的用Qt编写TCP协议程序的方法。