最近有个需求,需要做一个桌面插件,其实是一个微型桌面应用,可以集成第三方的接口,比如网络摄像头,身份证读卡器等,同时又需要处理前端(谁站的离用户近谁就是前端,常见的是H5,或者其他什么不三不四的管理系统、网站什么的)过来的请求,不管谁了,反正有奶就是娘,人家需要你就去奶。同时还需要处理本地文件上传等需求,文件上传又引入另外一个开源裤curl 。把以上信息整理如下
- 需求
- http 服务;
- GUI 用户界面(摄像头信息预览等);
- 其他网络操作(比如文件上传,获取参数);
- 集成第三方接口;
- 结构设计
- 方案一是服务+业务的标准流程,而且都是在Windows 平台下实现,正常没这么干的。正常需求从接口流进来,到调度过滤转发,然后业务系统处理任务并更新状态,调度拿到任务状态再推给用户,整个流程结束。这个方案有点儿小题大做了,硬性剥离开服务和业务,而且实现起来人为增加了复杂度。惯性思维+想的太多;
- 方案二叫一个应用或者一个服务+多个模块。因为整个应用,使用频率,并发要求并不高,几乎为0。也就是说只要是活的就行;
- 引入的依赖
- 配置部分,因为是MFC应用,使用的是WINAPI;
- http 服务,数据格式统一成json, 所以引入jsoncpp, 个人感觉不好用;
- http 服务,引入mongoose,体量小,操作简单的web服务器。用soket 也可以,可能我的打开方式有问题。前期用socket ,拼接的格式总有问题,各种坑,自己也对协议的具体格式不是很了解,项目需要,也没有那么多时间去研究协议,所以果断放弃,改用mongoose;
- 其他的网络服务,http get/post 都是用curl 实现的;
- 第三方依赖;
- 程序的整体结构,技术细节先不讨论,具体会单写文章讨论,这里综述一下整个项目流程,用到部分C11的特性
- http 服务;
本地封装一个httpServer,提供start(), close()等函数支持调用
bool HttpServer::Start()
{
std::thread t([&](){
mg_mgr_init(&m_mgr, NULL);
mg_connection *connection = mg_bind(&m_mgr, m_port.c_str(), OnHttpEvent);
if (connection == NULL)
return false;
mg_set_protocol_http_websocket(connection);
printf("starting http server at port: %s\n", m_port.c_str());
// loop
while (true)
{
if (m_bExitSvr)
{
break;
}
mg_mgr_poll(&m_mgr, 500); // ms
}
return true;
});
t.detach();
return true;
}
bool HttpServer::Close()
{
m_bExitSvr = true;
Sleep(1000);
mg_mgr_free(&m_mgr);
return true;
}
启动服务和注册外部回调(用于交互)
void CCtWebCameraDlgApp::startHttpSvr()
{
std::string port = "6080";
http_server = std::shared_ptr<HttpServer>(new HttpServer);
http_server->Init(port);
// 摄像头采集
http_server->AddHandler("/api/camera", [&](std::string url, std::string body, mg_connection *c, OnRspCallback rsp_callback){
int iState(200);
std::string str("init data.");
CCtWebCameraDlgDlg *dlg = (CCtWebCameraDlgDlg *)theApp.m_pMainWnd;
if (dlg != nullptr)
{
std::string strType = getImgType();
dlg->getImgBase64(strType.c_str(), str, iState);
}
rsp_callback(c, str, iState);
return true;
});
// 身份证信息采集
http_server->AddHandler("/api/idcard", [&](std::string url, std::string body, mg_connection *c, OnRspCallback rsp_callback){
int iState(200);
std::string str("init data.");
CCtWebCameraDlgDlg *dlg = (CCtWebCameraDlgDlg *)theApp.m_pMainWnd;
if (dlg != nullptr)
{
dlg->getIdCardInfo(str, iState);
}
rsp_callback(c, str, iState);
return true;
});
http_server->Start();
}
处理http协议
void HttpServer::OnHttpEvent(mg_connection *connection, int event_type, void *event_data)
{
http_message *http_req = (http_message *)event_data;
switch (event_type)
{
case MG_EV_HTTP_REQUEST:
HandleEvent(connection, http_req);
break;
case MG_EV_RECV: // tcp
break;
default:
break;
}
}
处理请求并响应
void HttpServer::HandleEvent(mg_connection *connection, http_message *http_req)
{
std::string req_str = std::string(http_req->message.p, http_req->message.len);
printf("got request: %s\n", req_str.c_str());
// 先过滤是否已注册的函数回调
std::string url = std::string(http_req->uri.p, http_req->uri.len);
std::string body = std::string(http_req->body.p, http_req->body.len);
std::string query = std::string(http_req->query_string.p, http_req->query_string.len);
char var[MAX_PATH];
var[0] = '\0';
mg_get_http_var(&http_req->query_string, "callback", var, sizeof(var)); //获取变量
m_strCallback = var;
mg_get_http_var(&http_req->query_string, "image_type", var, sizeof(var)); //获取变量
m_strImg_type = var;
auto it = s_handler_map.find(url);
if (it != s_handler_map.end())
{
ReqHandler handle_func = it->second;
handle_func(url, body, connection, SendRsp);
}
if (route_check(http_req, "/api/camera"))
{
SendRsp(connection, "connect success.", 200);
}
std::string req_str = std::string(http_req->message.p, http_req->message.len);
printf("got request: %s\n", req_str.c_str());
// 先过滤是否已注册的函数回调
std::string url = std::string(http_req->uri.p, http_req->uri.len);
std::string body = std::string(http_req->body.p, http_req->body.len);
std::string query = std::string(http_req->query_string.p, http_req->query_string.len);
char var[MAX_PATH];
var[0] = '\0';
mg_get_http_var(&http_req->query_string, "callback", var, sizeof(var)); //获取变量
m_strCallback = var;
mg_get_http_var(&http_req->query_string, "image_type", var, sizeof(var)); //获取变量
m_strImg_type = var;
auto it = s_handler_map.find(url);
if (it != s_handler_map.end())
{
ReqHandler handle_func = it->second;
handle_func(url, body, connection, SendRsp);
}
if (route_check(http_req, "/api/camera"))
{
SendRsp(connection, "connect success.", 200);
}
}
构造响应信息
void HttpServer::SendRsp(mg_connection *connection, std::string rsp, int state)
{
// 必须先发送header
mg_printf(connection, "%s", "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
std::string strSt;
strSt = "\r\n\"state\":" + std::to_string(state);
strSt += ",\r\n";
std::string strRep("([{");
strRep.append(strSt);
strRep.append("\"result\": %s \r\n}])");
std::string strRes(m_strCallback);
strRes.append(strRep);
// 以json形式返回
//mg_printf_http_chunk(connection, "{ \"result\": \"%s\"}", rsp.c_str());
mg_printf_http_chunk(connection, strRes.c_str(), rsp.c_str());
// 发送空白字符快,结束当前响应
mg_send_http_chunk(connection, "", 0);
}
服务部分到这里结束,基本的http请求都可以处理了。值得注意的是跨域访问的时候传入callback=whenreadover,我们填充json结构如下就可以解决跨域的问题
curl http文件上传, 这个是固定套路,没什么好说的,服务端只要接受对应格式的文件,基本不会有什么问题,亲测
int http_post_file(LPSTR url, LPSTR filename, LPSTR strImg, std::string& strRes)
{
assert(url != NULL);
assert(filename != NULL);
std::string strName = getfileNm(filename);
int ret = -1;
CURL *curl = NULL;
CURLcode code;
CURLFORMcode formCode;
int timeout = 15;
struct curl_httppost *post = NULL;
struct curl_httppost *last = NULL;
struct curl_slist *headerlist = NULL;
curl_formadd(&post, &last, CURLFORM_COPYNAME, "image_type",
CURLFORM_COPYCONTENTS, strImg,
CURLFORM_END);
curl_formadd(&post, &last, CURLFORM_COPYNAME, "file",
CURLFORM_FILE, filename,
CURLFORM_END);
curl_formadd(&post, &last,
CURLFORM_COPYNAME, "submit",
CURLFORM_COPYCONTENTS, "upload",
CURLFORM_END);
curl = curl_easy_init();
if (curl == NULL)
{
fprintf(stderr, "curl_easy_init() error./n");
goto out;
}
curl_easy_setopt(curl, CURLOPT_HEADER, 0);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPPOST, post);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &process_storage_server_data);
code = curl_easy_perform(curl);
if (code != CURLE_OK)
{
fprintf(stderr, "curl_easy_perform[%d] error./n", code);
goto all;
}
ret = 0;
all:
curl_easy_cleanup(curl);
out:
curl_formfree(post);
strRes = g_StroageServerResponseResult;
return ret;
}
- 这个服务的测试,通过nodejs/curl 发送http 请求,文件上传部分通过formidable 做测试
- 安装nodejs 运行http_post.js 发送HTTP post 请求,关于nodejs 发送HTTP请求,有篇文章写的特别好,https://my.oschina.net/freddon/blog/513853
var http = require('http');
var querystring = require('querystring');
var post_data = {
image_type: 1,
time: new Date().getTime()};//这是需要提交的数据
var content = querystring.stringify(post_data);
var options = {
hostname: '127.0.0.1',
port: 6080,
path: '/api/camera',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
};
callback = function(res){
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log("function call back [ing].......");
console.log('BODY: ' + chunk);
});
res.on('end', function (chunk){
console.log("callback end...........");
console.log('body' + chunk);
});
}
var req = http.request(options, callback);
req.on('error', function (e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write(content);
req.end();
console.log("start http_post to start camera.")
- 通过curl.exe 发送HTTP post 请求
curl -x 127.0.0.1:8888 -F "file=@1.jpg" -F "image_type=1" http://timing.1qjd.com/Api/Data/uploadInfoImage
# curl 为Windows平台,编译出来的可执行文件,通过命令行形式调用;
#127.0.0.1:8888 设置代理信息,通过一些抓包工具比如Findler可以捕捉到请求;需要开启本地代理服务,这个稍后会单独讲;
# -F 为请求参数,这里不太确定@是什么意思,因为1.jpg和curl.exe 同路径,所以可以直接传文件名,否则应该指定文件全路径;
# http://timing....... 为服务器地址
# 上图蓝色部分为服务器响应信息;
以上零零散散,写的比较乱,结构不够完整,慢慢进步。