最近在做骨料全自动自动装车项目,项目需要对车辆进行定位,对车厢尺寸进行测量,因此用到了激光雷达。具体的做法,在linux工控机上运行一个程序,使用qt,C++开发,对接激光雷达处理数据,得到车辆尺寸,然后将尺寸发送给软件系统。这里工控机和激光雷达组成一个独立的扫描系统。(激光雷达这个程序随后再共享吧)
原来测量系统和上位软件系统通讯,采用了tcp方式,通过发送自定义指令,测量,并返回数据。后来发现这种方式,需要不停的去维护tcp链接,断线重连,上位机维护比较复杂,后来想到给激光雷达系统弄成http方式的,上位使用get请求,就可以拿到测量结果,比较方便。因此用C++写了一个http服务。
由于前面对http协议进行过比较详细的解析,因此本次也比较简单,http协议解析见:使用java手写一个http服务,Http协议解析_javahttp服务-CSDN博客
这里具体的思路其实很简单:
step 1: 程序启动,读取配置文件,监听端口,等待客户端链接。主要代码如下:
/**
* @brief HttpServer::start 开启服务器
*/
void HttpServer::start(){
if(svr->isListening()){
return;//已经开始了,就不要再监听了
}
int port=Config::getInstance()->value("server/port").toInt();//获取端口
if(!svr->listen(QHostAddress::Any,port)){
qDebug()<<"started http server on port"<<port<<"error";
}
else {
qDebug()<<"listsen port"<<port<<"success";
}
}
step 2: 当有客户端链接进来,在线程池(QThreadPool)启动一个request处理对象(QRunable),解析客户端发过来的请求数据,这里主要解析请求地址,主要代码如下:
/**
* @brief QServer::onConnect 新的链接,客户端
*/
void HttpServer::onConnect(){
if(svr->hasPendingConnections()){ //如果服务端有一个待处理的连接,就返回真,否则返回假
QTcpSocket* socket = svr->nextPendingConnection(); //将下一个挂起的连接作为已连接的qtcsocket对象返回。
if(!socket->isValid()){ //套接字是否有效
qDebug()<<"invalid socket";
return;
}
Request* r=new Request(socket);//创建一个request对象,处理请求数据
connect(r,SIGNAL(onFinish(Request*)),this,SLOT(onRequestFinish(Request*)));//用于监听请求处理完毕消息
QThreadPool::globalInstance()->start((QRunnable*)r);//交给线程池处理
}
else{
qDebug()<<"client err";
}
}
step 3:从配置文件读取网址根目录,加上当前地址,读取文件,作为请求返回的body。如果文件不存在,把请求返回code,设置为404,就是没有这个文件。如果有,请求返回code设置为200,就是正常。这一部分代码稍微长点。
/**
* @brief Request::doResponse 该方法由主线程调用,因为客户端是在主线程产生的,不能跨线程调用
*/
void Request::doResponse(){
this->response->doResponse();//response返回
}
/**
* @brief RequestHandler::run 处理主函数,这是在子线程运行的,不耽误主线程同时处理多个请求
*/
void Request::run(){
//step 1 接收数据
//qDebug()<<"handle request data";
map=new QMap<QString,QString>();//用来存储header的map集合
QByteArray arr=client->readLine();//读取一行
int c=5;
while(arr.length()==0 && c-->0){//防止没有读取到数据
QThread::msleep(20);//等待20毫秒,防止没有数据
arr=client->readLine();//读取一行
}
if(arr.length()==0){//如果还是读取不到
//没有任何数据
response->setCode(200);
response->setContentType("application/json");
response->setBody(QString("{\"code\":9999,\"msg\":\"here is some thing worng\"}"));
emit onFinish(this);//告诉主线程,处理完了
return;
}
QString url(arr);//转换为字符串
QStringList slist=url.split(" ");
path=slist.at(1);//请求路径
//step 2解析header, 虽然并没有啥用
while(!client->atEnd()){
arr=client->readLine();//读取行
if(arr.length()<3)continue;//跳过空行
QString l(arr);//转换为字符串
QStringList list=l.split(":");//按冒号分割
if(list.size()<2)continue;//如果没有冒号,直接跳过
QString k=list.at(0);//名称
QString v=list.at(1);//值
//qDebug()<<"header"<<k<<v;
map->insert(k.trimmed(),v.trimmed());//添加到map
}
//qDebug()<<"path"<<path;
if(path=="/")path=Config::getInstance()->value("server/default").toString();//默认访问文件
qDebug()<<"path"<<path;
QByteArray b=FileTool::getInstance()->readFile(path);//读取文件
response->setContentType(FileTool::getInstance()->getFileExt(path));//设置返回内容
response->setBody(b);//设置返回内容
if(b==NULL || b.size()==0){
response->setCode(404);//找不到就是404
}
else{
response->setCode(200);//正常返回
}
// request finish
emit onFinish(this);//请求结束
}
step 4:请求处理完成以后,emit一个信号,通知主线程,由主线程调用response返回。主要就是下面一行代码:
emit onFinish(this);//请求结束
step 5 :response 返回,主要就是根据,http协议格式,返回标准数据,详细的协议就不说了,代码如下:
/**
* @brief Response::doResponse 请求结果返回
*/
void Response::doResponse(){
QString d="HTTP/1.1 "+QString::number(code)+" OK\r\n";//返回第一行
client->write(d.toLatin1());
QString s="Content-Type: "+contentType+"\r\n";//返回类型行,类型不对的话,html不会正常显示
client->write(s.toLatin1());
client->write("Content-Language: zh-cn\r\n");
if(body==NULL || body.length()==0){//没有body
client->write("Content-Length: 0\r\n");//内容长度为0
client->write("\r\n");//后面有个空行
}
else {
QString d1=QString("Content-Length: %0\r\n").arg(body.length());//内容长度行
client->write(d1.toLatin1());
client->write("\r\n");//内容和body之间有个空行
client->write(body);
}
//response end
client->close();//close client
}
配置文件类Config使用QSetting,对QSetting进行了简单封装,代码如下:
#include "config.h"
Config::Config()
{
set=new QSettings("config.ini", QSettings::IniFormat);
}
///
/// \brief Config::login 登录判断
/// \param name 用户名
/// \param pwd 密码
/// \return
///
bool Config::login(QString name, QString pwd){
if(name==set->value("server/user").toString() && pwd==set->value("server/password").toString()){
return true;
}
else return false;
}
///
/// \brief Config::getInstance 单例模式
/// \return
///
Config* Config::getInstance(){
static Config* ins=NULL;
if(ins==NULL)ins=new Config();
return ins;
}
///
/// \brief Config::value 获取一个值
/// \param key
/// \return
///
QVariant Config::value(QString key)
{
return set->value(key);
}
///
/// \brief Config::setValue 保存一个值
/// \param key 键
/// \param value 值,整数
///
void Config::setValue(QString key, int value){
set->setValue(key,value);
save();
}
///
/// \brief Config::setValue 保存一个值
/// \param key 键
/// \param value 值,字符串
///
void Config::setValue(QString key,QString value){
set->setValue(key,value);
save();
}
///
/// \brief Config::maxClients 返回最大运行连接数
/// \return
///
int Config::maxClients(){
return set->value("server/maxClients").toInt();
}
///
/// \brief Config::save 保存配置
///
void Config::save(){
set->sync();
}
///
/// \brief Config::sync 保存配置
///
void Config::sync(){
set->sync();
}
///
/// \brief Config::groups 返回所有组
/// \return
///
QStringList Config::groups(){
return set->childGroups();
}
配置文件格式:
[server]
port=8182
root=e:/temProject/svrtest/
default=index.html
写了一个简单的网页进行测试。服务端运行后截图如下:
html网页可以正常显示: