最近做了一个接口软件的二次开发还有动态链接库的修改和调试, 涉及到 HTTP 通讯多线程第三方库的使用等一些知识, 接下来大概会有数篇文章分别记录一下这个过程以及其中遇到的问题, 梳理一下不求甚解的部分, 以待巩固学习
大问题没有小毛病不少, 切实感受到不乐意读英文文档的坏处, 以后应该好好培养一下了, 不能看到英文就不想读_(:з)_效率太低
内容简述
接口设计: HTTP 获取 RSA 加密并 Gzip 压缩过的 Json 串并解析出实时数据, 使用 std::map 做缓存, 在 SCADA 系统发送命令帧时根据集中器 ID 按规约组帧上传, 与系统间的通讯采用 TCP 方式, 相关配置及信息在软件启动时从 Access 读取
SCADA 系统 dll: 按新的需求修改编译原有 dll 并与接口软件和 XX 系统 (HTTP 提供数据方) 联调
一 HTTP 协议请求与响应头结构
1. 关于 HTTP 协议的几个理解
HTTP 协议是一个应用层协议无状态协议, 是由请求和响应构成的标准 C/S 模型;
HTTP 协议基于 TCP 协议(TCP 协议是传输层的有状态协议), 其间加入 TLS/SSL 协议后则是 HTTPS 协议, 默认 HTTP 端口为 80,HTTPS 端口为 443;
HTTP 简要流程: TCP 三次握手建立连接客户端发送 HTTP 请求服务端确认并发送数据
HTTP 常见状态码: 200(请求成功),3xx(重定向),400(语法错误),403(拒绝执行),404(未找到资源),5xx(服务器错误), 详细信息参考:[HTTP 状态码详解] [HTTP 状态码];
2.HTTP 请求头和响应头的结构
通常 HTTP 消息包括客户机向服务器的请求消息和服务器向客户机的响应消息这两种类型的消息由一个起始行, 一个或者多个头域, 一个只是头域结束的空行和可选的消息体组成 HTTP 的头域包括通用头, 请求头, 响应头和实体头四个部分每个头域由一个域名, 冒号 (:) 和域值三部分组成域名是大小写无关的, 域值前可以添加任何数量的空格符, 头域可以被扩展为多行, 在每行开始处, 使用至少一个空格或制表符详细信息参考:[HTTP 头部详解]
请求头样例:
Get www.sina.com.cn:80
需要注意的是对于 HTTP/1.1 标准来说, Host 头域是必须的, 否则将返回 400 错误
响应头样例:
Request.png
一般来说常见的成功 HTTP 响应返回的状态码都是 200
二使用 ACL 库的 acl_pool_request 样例实现 HTTP 管理线程
关于 ACL 库:
[作者博客]
[Github]
原 acl_pool_reuqest 样例 in Github, 注意此样例中并未添加 Host 头域的内容, 因此响应体状态码必然是 400 格式错误: main.cpp
项目使用相关代码如下, 这部分大体设计就是软件启动时开一个工作线程来管理 HTTP 通讯, 当 SCADA 要求集中器 ID 或数据在缓存中不存在时执行立即请求(集中器 ID 不存在时还要更新数据库和缓存的 key), 否则就一直遍历 map 缓存的 key 请求实时数据来更新 value:
- static connect_pool* __conn_pool = NULL;
- static acl_pthread_pool_t* __thr_pool = NULL;
- static bool __unzip = false;
- // 初始化过程
- static void init(const char* addr, int count)
- {
- // 创建 HTTP 请求连接池对象
- __conn_pool = new http_request_pool(addr, count);
- // 创建线程池
- __thr_pool = acl_thread_pool_create(count, 60);
- }
- // 进程退出前清理资源
- static void end(void)
- {
- // 销毁线程池
- acl_pthread_pool_destroy(__thr_pool);
- // 销毁连接池
- delete __conn_pool;
- }
- // HTTP 请求过程, 向服务器发送请求后从服务器读取响应数据
- static bool http_get(http_request* conn, int ID)
- {
- char chURL[256] = { 0 };
- sprintf(chURL, "此处为 URL=%d", ID);
- // 创建 HTTP 请求头数据
- http_header& header = conn->request_header();
- header.set_host("此处为 host 地址");
- header.set_url(chURL)
- .set_keep_alive(true)
- .set_method(HTTP_METHOD_GET)
- .accept_gzip(__unzip);
- // 发送 HTTP 请求数据同时接收 HTTP 响应头
- if (conn->request(NULL, 0) == false)
- {
- // 打印日志
- return false;
- }
- // 判断状态字
- int status = conn->http_status();
- if (status != 200)
- {
- // 打印日志
- return false;
- }
- // ... variables definition
- // 接收 HTTP 响应体数据
- while (true)
- {
- ret = conn->read_body(szGzipBUF, sizeof(szGzipBUF));
- if (ret == 0)
- {
- if (conn->body_finish())
- break;
- }
- else if (ret < 0)
- {
- // 打印日志
- return false;
- }
- length += ret;
- }
- // ... 数据处理: 解压解密解码解析并更新缓存
- return true;
- }
- // 线程处理过程, 回调函数
- static void thread_main(void* ID)
- {
- for (auto iter : mapCacheData)
- {
- // 从连接池中获取一个 HTTP 连接
- http_request* conn = (http_request*)__conn_pool->peek();
- if (conn == NULL)
- {
- // 打印日志
- break;
- }
- // 需要对获得的连接重置状态, 以清除上次请求过程的临时数据
- else
- conn->reset();
- // 开始新的 HTTP 请求过程
- // 当传入当前收到集中器地址时不再遍历连接
- if (ID)
- {
- if (http_get(conn, *(int *)ID) == false)
- {
- // 立即请求失败, 重发
- nGetFailedCount++;
- // 错误连接需要关闭
- __conn_pool->put(conn, false);
- }
- else
- {
- nGetFailedCount = 0;
- __conn_pool->put(conn, true);
- }
- break;
- }
- else
- {
- if (http_get(conn, iter.first) == false)
- {
- // 错误连接需要关闭
- __conn_pool->put(conn, false);
- }
- else
- __conn_pool->put(conn, true);
- }
- }
- }
- static void run(bool flag)
- {
- // 向线程池中添加任务
- //for (int i = 0; i < cocurrent; i++)
- // acl_pthread_pool_add(__thr_pool, thread_main, NULL);
- if (flag)
- acl_pthread_pool_add(__thr_pool, thread_main, (void *)&nJZQID);// 立即请求, 单线程单连接
- else
- acl_pthread_pool_add(__thr_pool, thread_main, NULL);// 遍历请求, 单线程多连接
- }
- // HTTP 管理线程, 持续请求数据至缓存
- DWORD WINAPI CDBDataUpLoadDlg::HTTPProc(LPVOID lpParameter)
- {
- CDBDataUpLoadDlg *p = (CDBDataUpLoadDlg*)lpParameter;
- string addr("此处为 host 地址: 80");
- int cocurrent;// 连接池里的最大连接数线程池里的最大线程数
- while (TRUE)
- {
- cocurrent = mapCacheData.size();// 要采集的集中器数量
- init(addr, cocurrent);
- run(bRequestFlag);
- end();
- if (bRequestFlag) // 立即请求
- {
- if (nGetFailedCount == 3)// 三次请求失败
- {
- EnterCriticalSection(&g_csInfo);
- vecInfo.push_back("此处为软件显示信息, 在定时器中定时显示并清空");
- LeaveCriticalSection(&g_csInfo);
- bRequestFlag = false;
- }
- else if (nGetFailedCount == 0)
- {
- SetEvent(p->m_hEventTcpSend);// 获取到立即请求数据后触发事件
- bRequestFlag = false;
- }
- }
- }
- return 0;
- }
使用时遇到的一些注意点:
1. 创建 HTTP 请求头数据时必须有 host, 可以直接写进完整的 URL 也可以用 set_host 成员函数添加, 一开始以为 init 后就默认添加了 host 真是 naive 了;
2.ACL 提供了多种读取 HTTP 响应体数据的函数用于不同的存储结构读取方式以及是否自动解压, 这次开发用到的外部库都是静态链接的方式, 但是 ACL 库的解压器貌似必须加载一个自己的 dll, 使用的时候各种出问题, 无论使不使用 ACL 的解压得到的密文都无法正确解密, 最后还是获取原始数据
int read_body(char * buf, size_t size);
然后用 Zlib 来做了解压;
3.ACL 库中使用的 inet_ntop[InetNtop function(Windows)]函数在 Windows Server 2008/Windows Vista 以下的 ws2_32.dll 中未实现, 如果有 xp 或者 server2003 系统环境的需求还是不要用 ACL 比较好, 此外该函数在低版本中可以用 inet_ntoa 代替 (不支持 ipv6) 或者自己实现也未尝不可;
需要提高的地方:
1. 关于 MFC 的多线程同步目前用得比较多的只有事件非成员变量和临界区, 信号量互斥量和自定义消息有待熟练;
2. 为了访问类成员变量需要定义线程函数为类的静态成员函数, 通过传入 this 指针来访问类的非静态成员, 感觉很不美, 回头看看有没有更好的处理方案
来源: http://www.jianshu.com/p/db55b7bcc856