Qt5.8《网络版够级游戏》编写日志之四:服务器端登陆响应功能实现(TCP多线程)
登陆功能实现,需要服务器与客户端的交互才能实现,这里先实现服务器端的登陆响应功能,这样客户端就可以进行响应开发了,不然客户端的任何操作都无法得到响应。客户端和服务器端采用TCP协议实现,同时为了响应多客户端同时响应,这里服务器采用多线程模式,确保每一个客户端都有一个线程与之对应,不过多线程也会带来很多麻烦,主要在线程通信上。
1 服务器响应客户端信息处理线程类实现(SInfoProThread)
1.1 概述
为了实现服务器多线程响应客户端的连接和数据交互,那就必须先写一个线程类,这个线程类用来与客户端进行TCP通信,同时也表明是一个个客户端的连接,因此这个线程类需要保存各个客户端用户的连接信息,同时这个线程类需要将收到的信息与服务器主界面(我的命名是ServerFrame)进行交互,另外也需要对客户端的信息进行回应。这个Tcp通信线程类,我取名叫SInfoProThread,总结来说SInfoProThread主要完成以下几个功能:
1) 继承子QThread类;
2) 接收来自客户端的信息,并对信息进行响应;
3) 与服务器主界面类进行信息交互;
4) 保存客户端连接用户的相关信息,如登录名、登陆Ip、头像等。
1.2 代码实现
1.2.1 成员
qint32 ThreadID;//线程编号,自定义的线程编号,用以对所有连接线程进行区分
QString LoginName;//客户端登陆者用户名,通过登陆者用户名对各个连接线程进行区分
QString LoginIP;//客户端登陆者的IP
qint32 LoginHeadIcon;//客户端登陆者的头像索引
int socketDescriptor;//客户端登陆者连接套接字描述符
QTcpSocket *AnswerClient;//与客户端连接通信所用的Tcp套接字
1.2.2 信号
SInfoProThread类中,定义了两个自定义信号,分别是:
void sendMsg(qint32 Direct,QString Msg);
void sendClientExchMsg(ORDERTYPEnType,QString Msg);
信号sendMsg主要用于发送需要显示的信息给主界面显示,Direct表示在哪个地方显示,目前想到可显示信息的区域,一是服务器信息列表,二是在房间中显示。Msg表示需要显示的信息。
信号sendClientExchMsg主要用于发送与主界面类(ServerFrame类)进行交互的信息类,ORDERTYPE是我自定义的一个枚举类型,涵盖了可能用到的一些命令名称,我将其定义放在Global类中,Msg为编码后的信息内容,ORDERTYPE的定义如下:
enum ORDERTYPE
{ SERVERSTOP,ONLINE,OFFLINE,CHATTOALL,
CHATTOONE,CHATP2P,CHATREQUEST
};
我暂时能想到的命令信息大概有这么多,后续还将进一步完善,意思解释如下:
SERVERSTOP:服务器关闭
ONLINE:客户端上线
OFFLINE:客户端离线
CHATTOALL:聊天室中与所有聊天
CHATTOONE:聊天室中与某个人聊天
CHATP2P:点对点两人私下聊天
CHATREQUEST:点对点聊天应答
1.2.3 槽函数
1.2.3.1 answerClientDataSlot()
在SInfoThread线程类中定义了一个QTcpSocket *AnswerClient,用于接收从客户端送来的各类信息和数据,而answerClientDataSlot()就是为了接收和处理来自AnswerClient的信息和数据。目前,仅实现登陆功能,因此在该操函数中,先实现了登陆信息的处理和响应。
void SInfoProThread::answerClientDataSlot()
{
QByteArray Buffer;
QString Temp,Name,Password,str;
qint32 HeadIcon;
Temp = "";
Name = "";
Password = "";
HeadIcon = 0;
//读取缓冲区数据,每次读一行
Buffer =AnswerClient->readLine();
//响应一次,循环读完,之前我没有采用循环读,然后就会出现同时来的数据被一同读出来的情况
while(!Buffer.isEmpty())
{
//进行将信息进行本地8位字符串转换,这是为了支持中文的信息
Temp = QString::fromLocal8Bit(Buffer);
//登陆信息客户端发送过来的信息格式是LOGIN:Name:Password,也就是以LOGIN开头,后面跟姓名和密码,中间以:间隔
//因此,这里使用Qstring的split函数将各个信息区分出来
QStringList StrList = Temp.split(":");
//登陆信息处理
if(StrList.value(0) == "LOGIN")
{
//登陆信息客户端发送过来的信息格式是LOGIN:Name:Password,也就是以LOGIN开头,后面跟姓名和密码,中间以:间隔
Name = StrList.value(1);
Password = StrList.value(2);
//将客户端用户名和密码作为参数,调用全局配置类实例中数据库实例,然后调用其登陆方法,并据此得到返回值
qint32 ret = ServerConfig->ServerDataBase.loginServer(Name,Password);
//在《网络版够级》编写日志之三的文章中说了,为了取巧,我将用户的头像索引号放在返回值中,因此这时候需要进行处理得到头像索引,以及登陆成功与否的信息
//这里取整可得到登陆成功与否的返回值,取余则得到头像索引
switch (ret/100)
{
case 0:
//登陆值是0,标识用户名或者密码错误
Temp.clear();
//登陆不成功,因此加工好用户名或密码错误的信息发送至客户端
Temp ="LOGIN:LOGIN_USERPWDERROR";
//将SInfoThread,也就是自己本身加入到OnLineUser一个QMap中OnLineUser是Qmap <Qstring,QSInfoThread*>的一个实例,全局的,用于记
//录已经连接的用户和登陆的用户,因为没有登陆成功,所以用连接线程的线程ID来作为键值,录入该登陆线程
str.sprintf("NotLogin%d",ThreadID);
if(!OnLineUser.contains(str))
OnLineUser.insert(str,this);
//登陆不成功信息通过AnswerClient送回客户端
AnswerClient->write(Temp.toLocal8Bit());
AnswerClient->flush();
break;
case 1:
//返回值是1,表示该用户已经登陆了;将该信息传回客户端即可方法、流程与用户名、密码错误是一样的
Temp.clear();
Temp = "LOGIN:LOGIN_RELOGIN";
str.sprintf("NotLogin%d",ThreadID);
if(!OnLineUser.contains(str))
OnLineUser.insert(str,this);
AnswerClient->write(Temp.toLocal8Bit());
AnswerClient->flush();
break;
case 2:
//返回值是2,表示登陆成功
Temp.clear();
//登陆成功,这时给SInfoThread的成员变量LoginName(客户端登陆名)、头像索引进行赋值,这样SInfoThread就能同LoginName与其他客户端
//的连接线程区分出来了
LoginName = Name;
LoginHeadIcon = ret %100;
str.sprintf("%d",LoginHeadIcon);
Temp ="LOGIN:LOGIN_OK:"+Name+":"+LoginIP+":"+str;
//对于登陆成功的用户,以其登陆名为键值,将其线程的指针存入OnLineUser中,用户对所有连接和登陆玩家进行管理
if(!OnLineUser.contains(Name))
OnLineUser.insert(Name,this);
//客户登陆成功,服务器端需要更新玩家在线列表,因此通过sendClientExchMsg信号,将登陆用户的用户名和头像索引,转发给服务器主界面,由
//主界面相应的槽函数进行处理。
emit sendClientExchMsg(ONLINE,Name+":"+str+":"+LoginIP);//
//将登陆成功的信息,格式LOGIN:LOGIN_OK:Name:LoginIP+头像索引发送给连接的客户端
AnswerClient->write(Temp.toLocal8Bit());
AnswerClient->flush();
break;
default:
break;
}
}
}
后续有新的信息交互,仍然在此处增加处理代码。
1.2.3.2 initClientConnect()
客户端连接初始化操函数,这个槽函数以SInfoThread线程的start信号,作为启动信号,也就是SInfoThread启动时,该槽函数就随即运行。该槽函数主要是在线程启动后,对前面的Tcp套接字AnswerClient进行创建和设置,同时,在客户端连接时,将其IP记录到SinfoThread的成员变量LoginIP中,当用户正常登陆后,LoginIP也要显示到好友列表中。
void SInfoProThread::initClientConnect()
{
//对AnswerClient进行实例化
AnswerClient = new QTcpSocket;
AnswerClient->setSocketDescriptor(socketDescriptor);
//将客户端的IP记录下来
LoginIP = AnswerClient->peerAddress().toString();
QList <QString> IPTemp = LoginIP.split(":");
LoginIP = IPTemp.value(3);
//绑定槽函数,一个是AnswerClientreadyRead()信号与answerClientDataSlot()的槽的绑定,完成来自客//户端送来的信息处理和响应
connect(AnswerClient,SIGNAL(readyRead()),this,SLOT(answerClientDataSlot()),Qt::DirectConnection);
//这个是当客户端用户断开连接发生,促发disconnected()信号时,SinfoThread则以threadEndSlot()槽//来响应,完成用户退出连接时的一些操作,后面会细说
connect(AnswerClient,SIGNAL(disconnected()),this,SLOT(threadEndSlot()),Qt::DirectConnection);
}
1.2.3.2 threadEndSlot ()
客户端断开连接时信号响应槽函数,主要是对客户端断开连接后,进行响应,这里进行连接的客户端有两种情况,一个是成功登陆的,一种是只是连接上了,而没有成功登陆的,需要区分处理。
void SInfoProThread::threadEndSlot()
{
QString Temp;
//客户端离开服务器,要将该用户的登陆状态置为OffLine,这里没有区分成功登陆还是没登录的用户
//因为没登陆的用户其LoginName是空,也查不到,所这里统一都置OffLine状态,这样就没事了
ServerConfig->ServerDataBase.offLine(LoginName);
//向服务器主界面发送用户离开服务器的显示信息
emit sendMsg(0,LoginIP + ":" + LoginName + ":离开服务器!");
//根据LoginName是否是空来判断用户是否登陆
if(LoginName == "") //只是连接了,没登陆,因此LoginName为空
{
//没有登陆,就用ThreadID作为键值,把连接者从OnLineUser中移除
Temp = "";
Temp.sprintf("NotLogin%d",ThreadID);
if(OnLineUser.contains(Temp))
OnLineUser.remove(Temp);
}
else
{
//成功登陆的用户,则以LoginName作为键值,从OnLineUser中移除
if(OnLineUser.contains(LoginName))
{
//向主界面通报该用户退出,并从主界面树中清除,清除是不管在不在房间中,直接清除下线消息转发给主界面服务器,信息包含了下线者名称和其IP,主界面会对该信
//号响应
sendClientExchMsg(OFFLINE,LoginName + ":" + LoginIP);
OnLineUser.remove(LoginName);
}
}
//线程退出
deleteLater();
}
1.2.3.3 serverMsgProSlot (ORDERTYPEnType,QString Msg)
这个槽函数是用来响应服务器主界面送来的信息处理。在这儿我也把的信息传送思路整理了一下:
1) SInfoThread是一个个具体的客户端通信连接线程,它们负责从客户端接收信息,同时将服务器的响应回馈给客户端;
2) SInfoThread从客户端接收来的信息,需要转发给服务器端主界面,由主界面进行显示和其他处理;主界面需要向每个一个客户端发送消息或进行一些操作时,就发给一些指令+信息给每一个SInfoThread线程,由线程中的槽函数也就是serverMsgProSlot来进行响应,实现数据信息的执行解释和传输给客户端;
3) 总的来说,信息从客户端送到SInfoThread,SInfoThread再把信息转给主界面ServerFrame,然后主界面ServerFrame再对信息进行处理加工后再将信息回转给各个SInfoThread,再由每个SInfoThread进行适当的加工处理,然后将信息回馈给各个客户端;
大家可能觉得这样是不是绕了呢,为什么SInfoThread不直接回馈客户端信息,而需要通过主界面ServerFrame进行中转呢?一是因为这些信息首先要传到主界面ServerFrame进行显示,比如玩家列表信息;二是如果直接SInfoThread处理,也不能满足所有要求,比如一个玩家登陆后,服务器要告诉所有连接线程,向他们通知有一个用户登陆了,这样也需要进行信号槽设计,我这儿用主界面ServerFrame进行中转,在信号槽函数设计上,比较清晰,只是信息转了几次,不是太好跟踪。
serverMsgProSlot槽函数,目前只考虑到三种情况,一个上线、下线还有服务器关闭三个信息,后有的信息仍需在此处添加处理代码。见代码实现:
voidSInfoProThread::serverMsgProSlot(ORDERTYPE nType, QString Msg)
{
QString Temp,str;
QMap <QString,SInfoProThread*>::iterator it;
QList<SInfoProThread *> OnLineUserList;
//以147852为分隔符,分割每种信息,选择147852作为分隔符,是随便选的,是为了防止用现有的符号分割,后续如果玩家聊天信息中也输入了分割符号,这样信息就被分
//段了,用147852,毕竟概率要小的多,这个算是个隐患。
QList <QString> ChatList = Msg.split("147852");
switch (nType)
{
case SERVERSTOP:
//服务器停止信息,这里每个具体的SInfoProThread,都会给各自的客户端发送//一个服务器停止消息
Temp ="SERVERSTOP";
AnswerClient->write(Temp.toLocal8Bit());
AnswerClient->flush();
break;
case ONLINE:
Temp = "";
str = "";
//如果本线程维护的就是刚上线玩家,需要将所有已上线玩家信息都给他转一遍,这样它才能把玩家在线列表创建起来,后续还要将房间信息也 //转过去,//这里因为还有考虑到房间信息的事,所以只做了玩家信息的转发
if(LoginName == Msg)
{
//客户端收到的信息格式是:ONLINEYOU|Name:HeadIcon:IP|Name:HeadIcon:IP|……
Temp ="ONLINEYOU|";//刚上线玩家为ONLINEYOU,已上线玩家为ONLINE
OnLineUserList =OnLineUser.values();
//对在线玩家逐一按照格式加工转发信息
for(inti=0;i<OnLineUserList.size();i++)
{
Temp +=OnLineUserList.at(i)->LoginName + ":";
str.sprintf("%d",OnLineUserList.at(i)->LoginHeadIcon);
Temp += str + ":";
Temp +=OnLineUserList.at(i)->LoginIP + "|";
}
}
else //不是刚上线,那就只转发刚上线玩家信息
{
if(OnLineUser.contains(Msg))
{
//信息转发格式是:ONLINEYOU|Name:HeadIcon:IP| Name:HeadIcon:IP|……
//以Name找到刚上线玩家的具体信息
it =OnLineUser.find(Msg);
//按格式加工好要发送的信息
Temp ="ONLINE:";
Temp +=it.value()->LoginName + ":";
str.sprintf("%d",it.value()->LoginHeadIcon);
Temp += str + ":";
Temp +=it.value()->LoginIP;
}
}
//向客户端回送登陆成功的信息,主要都是已上线用户的信息
AnswerClient->write(Temp.toLocal8Bit());
AnswerClient->flush();
break;
case OFFLINE:
//下线信息为OFFLINE:Name\nIP,由ServerFrame加工好,直接回发给客户端即//可,无论谁下线,都需要告诉所有人
AnswerClient->write(Msg.toLocal8Bit());
AnswerClient->flush();
break;
default:
break;
}
}
至此,客户端通信连接线程类已经完成,实现从客户端接收信息,向服务器主界面转发信息,同时也完成服务器主界面给其发送信息的处理实现。
2服务器端多线程TCP服务器类实现
为了实现多线程响应客户端,因此需要在QTcpServer类基础上,编写一个自己的类(我命名为STcpServer),也就是说STcpServer的基类是QTcpServer,STcpServer主要是重载incomingConnection(qintptrsocketDescriptor)函数,并在这其中完成响应客户端的线程建立,也就是实例化一个个SInfoThread。实现每个客户端都有一个线程与它对应,为它服务。
STcpServer继承自QTcpServer,没有大的变化,只是重载了void incomingConnection(qintptr socketDescriptor)槽函数,在incomingConnection槽函数中完成客户端响应线程的建立,以及其他信息交互信号槽机制的绑定。代码如下:
void STcpServer::incomingConnection(qintptrsocketDescriptor)
{
static qint32ThreadID=100;
//有一个连接申请,即实例化一个SInfoProThread,并将套接字描述字传给它,这样SInfoProThread就可以创建每个客户端的套接字了,实现通信
SInfoProThread *thread = new SInfoProThread(socketDescriptor,0);
thread->ThreadID = ThreadID;
ThreadID++;
//每个用户的数据通信信息与主界面信息解释槽绑定
//此处的连接必须使用Qt::QueuedConnection队列方式,因为两个不同线程,在后面的槽中调用Socket
//不然会出现跨线程调用套接字情况,因为不用队列方式,默认为一个线程
qRegisterMetaType<ORDERTYPE>("ORDERTYPE");
//绑定主界面线程向客户连接线程信号槽
connect((ServerFrame *)(this->parent()),SIGNAL(sendSeverExchMsg(ORDERTYPE,QString)),
thread,SLOT(serverMsgProSlot(ORDERTYPE,QString)),Qt::QueuedConnection);
qRegisterMetaType<ORDERTYPE>("ORDERTYPE");
//用户上线后,给主界面发送信息,然后服务器在界面中添加玩家到列表,服务器再把该信息转发给所有已登陆用线程,也就是给所有在线用户发送该用户上线消息
connect(thread, SIGNAL(sendClientExchMsg(ORDERTYPE,QString)),(ServerFrame *)(this->parent()),
SLOT(clientMsgProSlot(ORDERTYPE,QString)),Qt::QueuedConnection);
//每个用户的数据通信信息与主界面信息解释槽绑定-需要在主界面线程显示的信息
connect(thread, SIGNAL(sendMsg(qint32 ,QString)), (ServerFrame*)(this->parent()), SLOT(showMsgSlot(qint32,QString)),Qt::QueuedConnection);
//线程启动后,进行初始化槽绑定
connect(thread, SIGNAL(started()), thread, SLOT(initClientConnect()));
//线程结束消息绑定
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();
}
至此,重载的STcpServer类实现完毕,在这个类中,主要就是重载incomingConnection函数,通过在这个函数,实现每个客户端连接线程SInfoThread的实例化,并为每个SInfoThread实例化线程,绑定与服务器主界面线程的信号-槽,后续一些槽这里先给出了,后续在主界面槽编写中,会逐步将处理连接线程SInfoThread送来的各类信号的槽实现完毕。
3 主界面线程类ServerFrame登陆功能配套实现
首先,说一下我为什么在服务器程序的主界面类上面加了线程二字,把它叫成主界面线程类,因为在刚开始使用Qt做多线程开发时,发现了Qt的线程机制与MFC不一样,对于主界面类中的界面元素的刷新只能在主界面这个类中开展,如果在别的地方调用主界面线程刷新,就会出现跨线程调用的问题,原因就是Qt中主界面类也是一个线程,而Qt不允许跨线程间调用,因此我自己都习惯把主界面类叫程主界面线程类,或者叫主线程。
有了SInfoTread和重载后的STcpServer类之后,多线程响应客户端的基本工作已经开展到位,下面就是将服务器端TcpServer服务启动了。刚才在前面也说了,STcpServer响应SInfoThread信号的一些槽要写好,另外,服务器自身启动服务也需要添加一些代码,下面开始实施。
3.1 主界面线程ServerFrame相关槽实现
3.1.1 clientMsgProSlot(ORDERTYPE nType,QString OrderInfo)
这个槽函数,主要是依据SInfoThread送来的各种信息并进行相应的处理。目前,我只做到登陆功能这一块,所以先把登陆这部分的功能进行实现,涉及到的信息有这么几个,一个上线信息ONLINE和下线信息OFFLINE。关于SInfoThread的信号格式在SInfoThread类中已经进行了说明,这里就把主界面线程收到每类信息所作的处理,在这里实现,代码如下:
void ServerFrame::clientMsgProSlot(ORDERTYPEnType, QString OrderInfo)
{
QString Temp;
QString Name;
QString IP;
qint32 HeadIcon;
QTreeWidgetItem * TempPlayer;
QList <QString> InfoList = OrderInfo.split(":");
QList <QTreeWidgetItem *> PlayListTemp;
switch (nType)
{
case SERVERSTOP:
break;
case ONLINE:
//是上线信息,从信息中提取用户名、头像索引以及用户IP
Name =InfoList.value(0);
HeadIcon =InfoList.value(1).toInt();
IP = InfoList.value(2);
//这里将上线用户添加到上线用户列表QTreeWidget中添加树控件子项
TempPlayer = newQTreeWidgetItem;
Temp.sprintf(":/HeadIcon/image/headIcon/%02d.png",HeadIcon+1);
//根据用户头像索引,设置其图标,因为想做成qq那样
TempPlayer->setIcon(0,QIcon(Temp));
Temp = Name +"\n" + IP;
TempPlayer->setText(0,Temp);
//添加到PlayerTree下面,PlayerTree是一个树节点,用户列表节点
PlayerTree->addChild(TempPlayer);
//因为需要将该用户登陆的信息告诉其他登陆者,所有这里需要把
//这个用户登陆的信息再转回给SInfoThread,由SInfoThread来转发
emit sendSeverExchMsg(ONLINE,Name);
break;
case OFFLINE:
//下线信息处理,获得用户名、用户IP
Name = InfoList.value(0);
IP = InfoList.value(1);
Temp = Name +":" + IP;
//向SInfoThread转发玩家下线信息
emit sendSeverExchMsg(OFFLINE,"OFFLINE:"+Temp);
Temp = Name +"\n" + IP;
//首先玩家树中查找到离开的玩家,找到后,将其中服务器玩家列表树中删除
//Qt::MatchRecursive表示搜索整个树,如果不加则只找一级
PlayListTemp = ui->PlayerTreeView->findItems(Temp,Qt::MatchExactly| Qt::MatchRecursive);
if(PlayListTemp.size()!=0)
{
//如果没有父节点,则直接删除
if(PlayListTemp.at(0)->parent() == Q_NULLPTR)
{
ui->PlayerTreeView->setCurrentItem(PlayListTemp.at(0));
deleteui->PlayerTreeView->takeTopLevelItem(ui->PlayerTreeView->currentIndex().row());
}
else //如果有父节点,则需要用父节点来takechild
{
//由于takechild的输入是该节点的行号,所以先将该节点设定为选中,这样就知道该节点的行号了
ui->PlayerTreeView->setCurrentItem(PlayListTemp.at(0));
delete PlayListTemp.at(0)->parent()->takeChild(ui->PlayerTreeView->currentIndex().row());
}
}
break;
default :
break;
}
}
3.1.2 showMsgSlot (qint32 Direct,QString Msg)
这个槽函数主要是完成信息的显示,之前SInfoThread、SDataBase、SconfigSet等类中都有一些信息需要在该槽函数中进行显示。Direct表示在那个控件中显示。目前,只做了服务器信息显示,Direct=0,就显示到服务器信息栏中,后续根据需要再添加。代码如下
void ServerFrame::showMsgSlot(qint32Direct,QString Msg)
{
QDateTime currentTime;
currentTime = QDateTime::currentDateTime();
switch (Direct)
{
case 0: //表示显示在服务器信息栏中
ui->EdtMessage->append(currentTime.toString() +":" +Msg);
break;
case 1: //表示显示在第一个房间信息栏中
default:
break;
}
}
3.2 主界面ServerFrame初始化及操作响应
3.2.1 主界面初始化
初始化主要在构造函数中进行,完成对一些成员的初始化以及对界面控件的一个初始化。代码比较简单,直接看代码即可。
ServerFrame::ServerFrame(QWidget *parent): QWidget(parent), ui(new Ui::ServerFrame)
{
ui->setupUi(this);
//初始化窗口1024*768
this->resize(1024,768);
//设置窗体名称
this->setWindowTitle("够级-服务器V1.0");
//加载配置文件
ServerConfig =new SConfigSet ;
//绑定ServerConfig信息显示信号到ServerFrame的显示槽上
connect(ServerConfig,SIGNAL(sendMsg(qint32,QString)),this,SLOT(showMsgSlot(qint32,QString)));
//绑定ServerDataBase信息显示信号到ServerFrame的显示槽上
connect(&(ServerConfig->ServerDataBase),SIGNAL(sendMsg(qint32,QString)),this,
SLOT(showMsgSlot(qint32,QString)));
//加载配置文件
ServerConfig->loadConfig();
//初始化界面,这里主要就是对玩家列表树控件进行了初始化,具体代码在后面
initUI();
//实例化STcpServer
MainServer = new STcpServer(this);
}
3.2.2 玩家列表树控件初始化
代码也比较简单,就是对玩家列表树控件,进行的简单的设置,增加了玩家节点和房间节点。代码如下:
void ServerFrame::initPlayerTree()
{
//隐藏表头
ui->PlayerTreeView->setHeaderHidden(true);
//设置节点图标大小
ui->PlayerTreeView->setIconSize(QSize(48,48));
//增加玩家列表节点
PlayerTree = new QTreeWidgetItem;
PlayerTree->setText(0,"玩家列表");
ui->PlayerTreeView->addTopLevelItem(PlayerTree);
//增加房间列表节点
RoomTree = new QTreeWidgetItem;
RoomTree->setText(0,"房间列表");
ui->PlayerTreeView->addTopLevelItem(RoomTree);
//设置树控件为展开
ui->PlayerTreeView->expandAll();
}
3.2.3 界面初始化initUI()
这里没什么具体代码,就是把界面初始化的东西都放在了这里,现在只是对玩家列表树控件,进行了初始化,也就是调用initPlayerTree。代码如下:
void ServerFrame::initUI()
{
ui->PsbStopServer->setEnabled(false);
initPlayerTree();
}
3.2.4 服务器启动按钮响应槽
启动按钮响应槽,主要完成服务器侦听的打开,以及“启动服务器”、“停止服务器”按钮的使能控制。代码如下:
voidServerFrame::on_PsbStartServer_clicked()
{
QDateTime currentTime;
currentTime = QDateTime::currentDateTime();
//先将所有用户都清空为未登陆,防止数据库中存储的玩家登陆状态有ONLINE的状态,因为有ONLINE的状态是不能登陆的,在启动服务器之前,统一全部清为未登陆
ServerConfig->ServerDataBase.offLine();
//打开Tcp侦听
bool flag = MainServer->listen(QHostAddress::Any,ServerConfig->ServerPort);
if(flag)
{
//将信息显示到信息显示栏中
ui->EdtMessage->append(currentTime.toString() +":" +"服务器启动正常!");
//进行“启动服务器”、“停止服务器”按钮的使能控制,防止重复操作
ui->PsbStartServer->setEnabled(false);
ui->PsbStopServer->setEnabled(true);
}
else
{
//将信息显示到信息显示栏中
ui->EdtMessage->append(currentTime.toString() +":" +"服务器启动异常!");
//进行“启动服务器”、“停止服务器”按钮的使能控制,防止重复操作
ui->PsbStartServer->setEnabled(true);
ui->PsbStopServer->setEnabled(false);
}
}
3.2.5 服务器停止按钮响应槽
停止按钮响应槽,主要完成服务器关闭,已连接用户线程关闭等工作,同时也完成“启动服务器”、“停止服务器”按钮的使能控制。代码如下:
voidServerFrame::on_PsbStopServer_clicked()
{
//将服务器关闭的消息通知给各个连接线程
emit sendSeverExchMsg(SERVERSTOP,"");
//将所有用户的登陆状态设为下线,也是为了防止出现有ONLINE的情况
ServerConfig->ServerDataBase.offLine();
//将所有打开的线程关闭,就是依据全局的玩家登陆进行维护
QList <QString> OnLineUserName = OnLineUser.keys();
QMap <QString,SInfoProThread*>::iterator it;
//依据键值,将响应的连接线程全部关闭
for(int i=0;i<OnLineUserName.size();i++)
{
it = OnLineUser.find(OnLineUserName.at(i));
it.value()->quit();
it.value()->wait();
}
//清空在线用户列表
OnLineUser.clear();
//清空在线用户列表
ui->PlayerTreeView->clear();
initPlayerTree();
//关闭服务器,服务器的作用只是打开端口,建立与客户端的连接
MainServer->close();
showMsgSlot(0,"服务器关闭成功!");
ui->PsbStartServer->setEnabled(true);
ui->PsbStopServer->setEnabled(false);
}
4 实现后的效果图展示
至此,服务器端登陆功能基本实现完毕。下面把效果展示一下,由于客户端还没弄,实际上也没有多少变化。