承接上章:Qt一步步搭建TcpServer0——序
要搭建网络库,那么肯定要从Server和Socket开始入手,本章就从封装QTcpServer和QTcpSocket开始入手,做好第一步工作。
其实图中注释描述的也比较清楚,能够较为直观体现出Server端的网络库多线程工作流程,我觉得也就没必要再过多解释了。
官网文档地址:QTcpServer
基本上是属于傻瓜式编程,大部分的工作都由底层给我们做好了。通常情况下,我们只需要监听一下端口,绑定个newConnection()就可以在新连接连入的时候获取到套接字:
int port = this->ui->spinBox->value();
QTcpServer *tcpserver = new QTcpServer();
tcpserver->listen(QHostAddress::Any, port);
connect(tcpserver, &QTcpServer::newConnection, this, &MainWindow::SlotNewConnection);
然后通过nextPendingConnection获取套接字就行了。
关于QTcpServer的设计其实主要有两种方式:
1、组合:如上面官方介绍所言,以组合的形式封装QTcpServer,然后连接newConnection信号槽,在槽函数里nextPendingConnection获取新socket,而这个socket将由QTcpServer来管理,自动删除:
2、继承:继承自QTcpServer,然后重载函数incomingConnection,在函数里自己去创建Socket对象并管理。
方式一和方式二其实都可以,但对我而言,我需要自定义socket,并且这个socket将会抛给上层,上层在整个过程中可能会存在需要使用到socket对象的情况,生命周期需要我的网络库来掌管,那么我将使用方式二。
新建项目,在pro文件中添加network库:
QT += core gui network
TcpServer封装自QTcpServer,基本功能底层已经做了很多,那么我们要做的就是做成能够直接用于项目需求上的接口:
头文件:
class TcpServer : public QTcpServer
{
Q_OBJECT
public:
TcpServer();
~TcpServer();
bool Start(int port);
void Stop();
size_t GetSessionSize() const ;
public:
//新连接回调
std::function<void(TcpSession*)> OnAccepted = nullptr;
protected:
virtual void incomingConnection(qintptr handle);
private:
bool IsRunning_ = false;
QThread *Thread_;
std::vector SessionList_;
};
一个Server首先需要提供的肯定是Start和Stop,这点不用多说,OnAccepted 是连接回调,通知上层,并把新连接丢给上层,上层拿到之后,是持有还是简单绑定回调就是上层的事情了。incomingConnection是需要重载的函数,qt的net库Accept后通知我们这层,我们再去创建socket(也就是TcpSession)。
cpp:
TcpServer::TcpServer()
{
}
TcpServer::~TcpServer()
{
this->Stop();
}
bool TcpServer::Start(int port)
{
if(IsRunning_)
return true;
Thread_ = new QThread();
Thread_->start();
//监听端口
if(!this->listen(QHostAddress::Any, (quint16)port))
return false;
IsRunning_ = true;
return true;
}
void TcpServer::Stop()
{
if(!IsRunning_)
return;
this->close();
Thread_->exit();
Thread_->wait();
delete Thread_;
for(TcpSession *session : this->SessionList_)
{
session->Disconnect();
}
for(TcpSession *session : this->SessionList_)
{
delete session;
}
this->SessionList_.clear();
Thread_ = nullptr;
IsRunning_ = false;
}
size_t TcpServer::GetSessionSize() const
{
return this->SessionList_.size();
}
void TcpServer::incomingConnection(qintptr handle)
{
TcpSession *session = new TcpSession();
session->setSocketDescriptor(handle);
session->moveToThread(this->Thread_);
this->SessionList_.push_back(session);
//通知上层
if(this->OnAccepted)
this->OnAccepted(session);
}
仔细一看,好嘛 ,我封装的也啥都没干,只是桥接了下启动和把新连接都丢到一个线程里去了。不着急,这只是第一步。当然,主要原因是该提供的接口函数QTcpServer已经提供了大部分功能(包括error回调也提供了)。
Stop函数,Stop函数主要是负责关闭线程,清理内存等,我在后面会讲,这也有一些需要补充的内容。在目前我们假设已经顺利关服,并且清理了Session。
此时存在一个问题,Session在OnAccept的时候需要往上抛,上层拿到之后可能会持有,写数据和disconnect可能不是在同一个线程。所以断开连接的时候,或者说关服的时候,正好业务层写数据,而session已经被析构了,这就可能存在隐患,所以这里引入shared_ptr:
头文件:
class TcpServer : public QTcpServer
{
Q_OBJECT
public:
TcpServer();
~TcpServer();
bool Start(int port);
void Stop();
size_t GetSessionSize() const ;
public:
//新连接回调
std::function<void(std::shared_ptr &)> OnAccepted = nullptr;
protected:
virtual void incomingConnection(qintptr handle);
private:
bool IsRunning_ = false;
QThread*Thread_;
std::vector<std::shared_ptr > SessionList_;
};
cpp:
TcpServer::TcpServer()
{
}
TcpServer::~TcpServer()
{
this->Stop();
}
bool TcpServer::Start(int port)
{
if(IsRunning_)
return true;
Thread_ = new QThread();
Thread_->start();
//监听端口
if(!this->listen(QHostAddress::Any, (quint16)port))
return false;
IsRunning_ = true;
return true;
}
void TcpServer::Stop()
{
if(!IsRunning_)
return;
this->close();
Thread_->exit();
Thread_->wait();
delete Thread_;
for(std::shared_ptr &session : this->SessionList_)
{
if(session.get())
session.get()->Disconnect();
}
this->SessionList_.clear();
Thread_ = nullptr;
IsRunning_ = false;
}
size_t TcpServer::GetSessionSize() const
{
return this->SessionList_.size();
}
void TcpServer::incomingConnection(qintptr handle)
{
std::shared_ptr session = std::make_shared();
session->setSocketDescriptor(handle);
session->moveToThread(this->Thread_);
this->SessionList_.push_back(session);
if(this->OnAccepted)
this->OnAccepted(session);
}
到这里,TcpServer的封装大致完成,上层可以直接用了,拿到Session之后也不用担心内存问题。
相对来说,TcpSession的封装更简单了,这里做封装目前没新增什么,之后可能会加入线程的指针或者别的数据等,不管怎样,还是自定义比较方便。
头文件:
#ifndef TCPSESSION_H
#define TCPSESSION_H
#include
#include
class TcpSession : public QTcpSocket
{
Q_OBJECT
signals:
void SignalRead(const char *data, int len);
public:
TcpSession();
~TcpSession();
void Disconnect();
qint64 Write(const char *data, qint64 len);
qint64 Write(const char *data);
protected:
private slots:
void SlotStartRead();
};
#endif // TCPSESSION_H
cpp:
TcpSession::TcpSession()
{
connect(this, &TcpSession::readyRead, this, &TcpSession::SlotStartRead);
}
TcpSession::~TcpSession()
{
disconnect(this, &TcpSession::readyRead, this, &TcpSession::SlotStartRead);
}
void TcpSession::Disconnect()
{
this->disconnectFromHost();
}
qint64 TcpSession::Write(const char *data, qint64 len)
{
return this->write(data, len);
}
qint64 TcpSession::Write(const char *data)
{
return this->write(data);
}
void TcpSession::SlotStartRead()
{
QByteArray buffer;
buffer = this->readAll();
emit this->SignalRead(buffer.toStdString().c_str(), buffer.length());
}
代码比较简单,没什么解释的必要。这里的工作,主要是统一了下发送数据和接收数据的接口参数,之后可能会自定义一个参数类型(看我有没有时间更),方便包的传输。
到这里,Socket和Server都封装好了,原本的信号槽也可以直接用。主界面写写就可以直接点击启动了,但我们能发现,目前我这里只是把所有的Session都丢到同一个线程去处理了。这就有很大的问题,假如十个,百个连接还好说,但是有千个万个连接呢?显然不能这么处理。
那么怎么办?其实在写博客之前,我也简单浏览了下别人的设计,大抵分为两派,一派是不处理,不用线程;另一派是一个连接一个线程。。。。。。也不是说不行,但是觉得不是特别好。
这个时候,就需要引入线程池,每个连接连入的时候,动态的根据线程池中线程负载情况来分配,尽量均衡的让一个线程处理多个连接,而我们只需要维护这几个线程就行了。那么下篇再说。