一、前言
前文介绍了UDP和TCP两种协议的服务器搭建方法,本文将基于UDP协议搭建服务器,实现黑白棋游戏的网络对战功能
二、基本功能
实现网络对战功能,要通过服务器进行两个客户端之间数据的转发。因此,需要绑定服务器IP地址和端口,同时要读取客户端的IP地址和端口进行接收和转发。
服务器功能如下:
IP地址和端口设置:用于设置服务器IP地址和端口号
在线列表:用于显示目前已经连接的客户端名称,IP地址,端口号
消息列表:显示上线,下线和端口绑定信息
删除功能:删除对应客户端
客户端功能如下:
落子吃子:与前文相同
IP地址和端口设定:用于连接服务器IP地址和端口号
用户名和对手用户名:用于指定对手进行网络对战
三、具体原理和详细实现
服务器
1.端口绑定
绑定端口通过QUdpSocket类中的bind()方法实现,该方法的作用是绑定后,每次UDP数据包到达指定地址和端口时,都会发出readyRead信号。
void Widget::on_btn_bind_clicked()
{
QString myIp = ui->ledit_ip->text();
QString myPort = ui->ledit_port->text();
QString msg;
bool ret = mySocket->bind(QHostAddress(myIp),myPort.toUInt());
if(!ret)
{
msg = "绑定失败";
}
else
{
msg = "绑定成功";
}
ui->tedit_message->append(msg);
}
2.删除功能
删除功能通过currentItem()确定选中项,通过contains()判断列表是否存在该条数据,通过removeAt()进行删除。
void Widget::on_btn_delete_clicked()
{
QListWidgetItem *cur = ui->lwid_inline->currentItem();
//先删除在线列表
for(int i = 0;i<listClient.length();i++)
{
if(cur->text().contains(listClient.at(i).name))
{
listClient.removeAt(i);
break;
}
}
int row = ui->lwid_inline->currentRow();
ui->lwid_inline->takeItem(row);
}
3.数据的接收和转发
数据的接收通过reaDatagram(),参数分别为数据报,最大字节,IP,端口号四个
其中最大字节可以通过pendingDatagramSize()设置,作用是返回上一个数据报的最大字节。
将数据包存储在字符串中,使用contains进行不同功能的筛选,该函数的功能是当字符串含有该关键字时返回1;
当检测到readyRead()信号时,服务器需要进行四种数据的筛选:
1.初始化:设置数据格式为init#from#to#role#initEnd,from表示数据来源,to表示数据发送对象,role表示当前落子,init和initEnd代表数据头和尾
查找在线列表中的用户名,筛选对应的用户进行数据报的发送,发送通过writeDatagarm(),第一个参数代表数据,第二个参数代表IP地址,第三个参数代表端口号,并通过消息列表提示初始化成功
//棋盘初始化 eg:init#from#to#role#initEnd
else if(msg.contains("init#"))
{
QStringList list = msg.split("#");
QString toname = list.at(2);
QString role = list.at(3);
for (int i=0;i<listClient.length();i++ )
{
if(listClient.at(i).name == toname)
{
QString buf = QString("init#%1#initEnd").arg(role);
mySocket->writeDatagram(buf.toUtf8(),
listClient.at(i).addr,
listClient.at(i).port);
QString msg = QString("通知棋盘初始化");
ui->tedit_message->append(msg);
break;
}
}
}
2.上线数据:inline#name#inlienEnd,name代表用户名
上线数据主要是加入在线列表和消息的显示
if(msg.contains("inline"))
{
QStringList list = msg.split("#");
member mb;
mb.addr = addr;
mb.port = port;
mb.name = list.at(1);
listClient.append(mb);
//记录上线
QString msg = QString("%1 [%2:%3] 上线!")
.arg(mb.name)
.arg(mb.addr.toString())
.arg(mb.port);
ui->tedit_message->append(msg);
//显示到在线列表中
ui->lwid_inline->clear();
for(int i=0;i<listClient.length();i++)
{
QString label = QString("%1#[%2%3]")
.arg(listClient.at(i).name)
.arg(listClient.at(i).addr.toString())
.arg(listClient.at(i).port);
ui->lwid_inline->addItem(label);
}
}
3.接收棋盘数据:data#from#to#x#y#role#daraEnd,x和y表示落子的坐标
//棋盘数据 eg:data#from#to#x#y#role#dataEnd
else if(msg.contains("data#"))
{
QStringList list = msg.split("#");
if(list.length()<=0)return;
QString toName = list.at(2);
for(int i = 0;i<listClient.length();i++)
{
if(listClient.at(i).name == toName)
{
mySocket->writeDatagram(msg.toUtf8(),
listClient.at(i).addr,listClient.at(i).port);
break;
}
}
}
4.下线数据:unline#name#unlineEnd:name代表用户名
//3.下线数据 unline#name#unlineEnd
else if(msg.contains("unline"))
{
QStringList list = msg.split("#");
QString name = list.at(1);
int i = 0;
for (; i < list.length(); i++)
{
if(name == listClient.at(i).name)
{
listClient.removeAt(i);
break;
}
}
//记录下线
QString msg = QString("%1 [%2:%3] 下线!")
.arg(listClient.at(i).name)
.arg(listClient.at(i).addr.toString())
.arg(listClient.at(i).port);
ui->tedit_message->append(msg);
//更新在线列表
ui->lwid_inline->clear();
for(int i = 0;i<listClient.length();i++)
{
QString label = QString("%1#[%2%3]")
.arg(listClient.at(i).name)
.arg(listClient.at(i).addr.toString())
.arg(listClient.at(i).port);
ui->lwid_inline->addItem(label);
}
}
客户端
1.连接服务器
向服务器发送上线数据,并将连接按钮设置为不可用
void ChessForm::on_btn_connect_clicked()
{
QString myIp = ui->ledit_ip->text();
QString myPort = ui->ledit_port->text();
QString myName = ui->ledit_name->text();
//1.上线数据 inline#name#inlineEnd
QString msg =QString("inline#%1#inlineEnd")
.arg(myName);
mySocket->writeDatagram(msg.toUtf8(),
QHostAddress(myIp),myPort.toUInt());
ui->btn_connect->setEnabled(false);
}
2.模式选择
当点击网络对战按钮时,将模式变量设置为NVN,将界面进行初始化,并向服务器发送上线数据报通知对方界面初始化。
void ChessForm::on_btn_nvn_clicked()
{
isDown = true;
currentPK = NVN;
//把界面初始化
if(ui->cbox_item->currentText().contains("白"))
{
setRole(chess::White);
}
else
{
setRole(chess::Black);
}
//把棋盘初始化
setChessInit();
//通知对方棋盘初始化 eg:init#from#to#role#initEnd
QString myIp = ui->ledit_ip->text();
QString myPort = ui->ledit_port->text();
QString myName = ui->ledit_name->text();
QString toname = ui->ledit_toname->text();
QString msg = QString("init#%1#%2#%3#initEnd")
.arg(myName).arg(toname).arg(currentRole);
mySocket->writeDatagram(msg.toUtf8(),QHostAddress(myIp),
myPort.toUInt());
}
3.数据接收
当服务器向界面转发数据的时候,进行数据报判断是初始化界面还是落子数据
初始化棋盘:调用写好的棋盘初始化方法
落子数据:接收对方落子位置,通过吃子方法判定,并更新数据。
//棋盘初始化数据 eg:init#role#initEnd
if(msg.contains("init#"))
{
QStringList list = msg.split("#");
if(QString(list.at(1)).toInt() == chess::White)
{
currentRole = chess::Black;
}
else if(QString(list.at(1)).toInt() == chess::Black)
{
currentRole = chess::White;
}
currentPK = NVN;
//把界面初始化
setRole(currentRole);
//把棋盘初始化
setChessInit();
}
//下棋数据 eg:data#from#to#x#y#role#dataEnd
else if(msg.contains("data#"))
{
QStringList list = msg.split("#");
int x = QString(list.at(3)).toUInt();
int y = QString(list.at(4)).toUInt();
int role = QString(list.at(5)).toUInt();
//更新界面数据
int ret = judegRule(x, y,formChessData,(chess::ChessType)role,true);
if(ret)
{
//把数据传送给棋盘类,用于更新界面
mychess->setChessStatus(formChessData);
//数据统计
ChessShow();
isDown = true;
}
}
4.落子判定
当选择网络对战模式时,每次落子需要向服务器发送落子信息,并进行界面更新
else
{
if(isDown)
{
int ret = judegRule(i,j,formChessData,currentRole,true);
if(ret)
{
//把数据传送给棋盘类,用于更新界面
mychess->setChessStatus(formChessData);
QString myIp = ui->ledit_ip->text();
QString myPort = ui->ledit_port->text();
QString myName = ui->ledit_name->text();
QString toName =ui->ledit_toname->text();
//通知对方客户端更新界面 eg:data#from#to#x#y#role#dataEnd
QString msg = QString("data#%1#%2#%3#%4#%5#dataEnd")
.arg(myName).arg(toName)
.arg(i).arg(j).arg(currentRole);
mySocket->writeDatagram(msg.toUtf8(),QHostAddress(myIp),myPort.toUInt());
isDown = false;
//数据统计
ChessShow();
}
}
}
四、效果展示
上线:
落子:
下线: