做过网页设计的人应该都知道ajax。
Ajax即Asynchronous Javascript And XML(异步的JavaScript和XML)。使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。
在IE浏览器中,Ajax技术就是基于JavaScript里面的XMLHttpRequest。AJAX通过XMLHttpRequest对象发出HTTP 请求,得到服务器返回的数据后,再进行处理。现在,服务器返回的都是JSON格式的数据,XML格式已经过时了,但是AJAX这个名字已经成了一个通用名词,字面含义已经消失了。
XMLHttpRequest对象是AJAX的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XML和Http,它实际上可以使用多种协议(比如file或ftp),发送任何格式的数据(包括字符串和二进制)。
其实,不仅在网页上能用JavaScript语言调用XMLHttpRequest组件,在桌面窗口程序里面也可以用C语言或C++调用XMLHttpRequest组件。
XMLHttpRequest是微软msxml6.0里面的组件。msxml6.0可直接解析服务器返回的xml文档,但json数据需要在网上找cJSON库来解析。
第一节 准备Web服务器页面
首先我们要在自己的服务器上准备好处理ajax请求的页面,本文准备了三个示例页面:str_test.php、json_test.php和xml_test.php,分别用来产生文本回应、json回应和xml回应。xml_test.php页面支持get和post两种ajax请求方式。
PHP文件用UTF-8编码保存,但C文件要用GB2312编码保存。
这是因为在微软的简体中文Windows系统里面,以A结尾的API函数(如CreateWindowA)采用的是GB2312编码的char *字符串,以W结尾的API函数(如CreateWindowW)采用的是UTF-16编码的wchar_t *字符串,COM组件使用的BSTR字符串是在wchar_t *字符串的基础上增加了4字节的字符串长度前缀。
IXMLHttpRequest组件(一种COM组件)工作时使用的是UTF-16编码的BSTR字符串。
我们在收到IXMLHttpRequest组件提供的UTF-16编码的BSTR字符串后,如果想要用printf在控制台上打印出来,就需要用convert_bstr_to_string函数将BSTR转换为GB2312编码的char *字符串。想要直接打印BSTR字符串不能用printf函数,要用WriteConsoleW函数才行。如果是显示在窗口的文本框或者窗口的标题栏上那就简单了,直接用SetWindowTextW或SetDlgItemTextW函数,把BSTR字符串传进去就行了。
同样,在给IXMLHttpRequest组件传递字符串参数时,需要先用convert_string_to_bstr函数将GB2312编码的char *字符串转换为UTF-16编码的带长度前缀的BSTR字符串。
如果是写入txt文本文件的话,我们就可以采用UTF-8编码,用MultiByteToWideChar函数(CP_UTF8)将BSTR转换成UTF-8编码的char *字符串,再用fprintf或fwrite写入txt文件。
str_test.php:
当前时间为。
json_test.php:
xml_test.php:
<?phpheader('Content-type: text/xml');session_start();header('Pragma: no-cache');header('Cache-control: no-cache');header('Expires: 0');echo '';$timestr = strftime("%Y%m%d%H%M%S");echo "";foreach ($_COOKIE as $name => $value) {$_name = htmlspecialchars($name);$_value = htmlspecialchars($value);echo "$_value";}foreach ($_GET as $name => $value) {$_name = htmlspecialchars($name);$_value = htmlspecialchars($value);echo "$_value";}foreach ($_POST as $name => $value) {$_name = htmlspecialchars($name);$_value = htmlspecialchars($value);echo "$_value";}echo '';?>
第二节 以同步方式发送Ajax请求
下面我们来看看C语言如何像网页里面那样用XMLHttpRequest发送ajax请求。
请注意IXMLHttpRequest和IXMLHTTPRequest是两个名字不同但内容完全相同的接口,可以互换使用。
/* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展名为c */#define COBJMACROS#include #include #pragma comment(lib, "msxml6.lib")// char *转BSTR// 用完后调用SysFreeString释放BSTR convert_string_to_bstr(const char *s){int n;wchar_t *ws;BSTR bstr = NULL;n = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0);ws = malloc(n * sizeof(wchar_t));if (ws != NULL){MultiByteToWideChar(CP_ACP, 0, s, -1, ws, n);bstr = SysAllocString(ws);free(ws);}return bstr;}// BSTR转char *// 用完后调用free释放char *convert_bstr_to_string(BSTR bstr){char *s;int n;n = WideCharToMultiByte(CP_ACP, 0, bstr, -1, NULL, 0, NULL, NULL);s = malloc(n);if (s != NULL)WideCharToMultiByte(CP_ACP, 0, bstr, -1, s, n, NULL, NULL);return s;}// 去掉字符串末尾的\r\nvoid remove_last_crlf(char *str){int len;len = (int)strlen(str);if (len >= 2 && str[len - 2] == '\r' && str[len - 1] == '\n')str[len - 2] = '\0';}// 示例: 读取纯文本内容 (同步方式)// 请注意有两个名称不同但内容完全一样的接口: IXMLHttpRequest和IXMLHTTPRequest, 随便用哪个都可以// 但class id只有一个: CLSID_XMLHTTPRequestvoid str_test(const char *url){char *response;long status;BSTR method_bstr, url_bstr, response_bstr;HRESULT hr;IXMLHttpRequest *xhr;VARIANT async; // VARIANT代表一个弱类型的变量VARIANT null;hr = CoCreateInstance(&CLSID_XMLHTTPRequest, NULL, CLSCTX_INPROC_SERVER, &IID_IXMLHttpRequest, &xhr);if (SUCCEEDED(hr)){// 打开连接// get参数内容是放到url字符串里面的method_bstr = convert_string_to_bstr("GET");url_bstr = convert_string_to_bstr(url);async.vt = VT_BOOL;async.boolVal = VARIANT_FALSE; // 选择非异步方式 (也就是同步方式)null.vt = VT_NULL;hr = IXMLHttpRequest_open(xhr, method_bstr, url_bstr, async, null, null); // 最后两个参数是用户名和密码, 不用填SysFreeString(method_bstr);SysFreeString(url_bstr);if (SUCCEEDED(hr))printf("open() succeeded\n");// 发送请求// 由于我们选择的是同步方式, 所以send函数要等到所有数据都接收完了才返回hr = IXMLHttpRequest_send(xhr, null); // 第二个参数是post数据内容, 不用管if (SUCCEEDED(hr))printf("send() succeeded\n");// 获取请求状态// 0: open函数还没调用// 1: send函数还没调用// 2: send函数调用了, get_status()函数已经可以使用了, 也可以读http headers了, 但是还读不了response// 3: 已收到了部分数据, 可以读responseBody和responseText// 4: 所有数据都已经接收完了, responseBody和responseText已经是完整数据了hr = IXMLHttpRequest_get_readyState(xhr, &status);if (SUCCEEDED(hr))printf("ready state: %d\n", status);// 获取http返回的response codehr = IXMLHttpRequest_get_status(xhr, &status);if (SUCCEEDED(hr))printf("http status code: %d\n", status);// 读取并显示文本内容hr = IXMLHttpRequest_get_responseText(xhr, &response_bstr);if (SUCCEEDED(hr)){// response_bstr是utf16编码, 转换后的response是gb2312编码// 把函数里面的CP_ACP改成CP_UTF8就可以转成utf8编码, 但是utf8就没办法用printf打印, 只能用fwrite写入txt文件再用记事本打开查看response = convert_bstr_to_string(response_bstr);remove_last_crlf(response);printf("response: %s\n", response);free(response);SysFreeString(response_bstr);}IXMLHttpRequest_Release(xhr);}printf("\n");}// 直接显示xml代码void display_xmlstr(IXMLDOMDocument *xmldoc){char *str;BSTR bstr;HRESULT hr;hr = IXMLDOMDocument_get_xml(xmldoc, &bstr);if (SUCCEEDED(hr)){str = convert_bstr_to_string(bstr);remove_last_crlf(str);printf("xmlstr: %s\n", str);free(str);SysFreeString(bstr);}}// 显示xml属性值void display_attr(IXMLDOMElement *elem, const char *name){char *value;BSTR bstr; // 表示一个带长度前缀的utf16编码的字符串VARIANT variant; // 表示一个弱类型的变量bstr = convert_string_to_bstr(name);IXMLDOMElement_getAttribute(elem, bstr, &variant);SysFreeString(bstr); // 使用完字符串后必须释放if (variant.vt == VT_BSTR){value = convert_bstr_to_string(variant.bstrVal);printf("%s: %s\n", name, value);free(value);}else if (variant.vt == VT_NULL)printf("%s: (null)\n", name);VariantClear(&variant);}// 检查xml节点名称是否为指定名称int check_node_name(IXMLDOMNode *node, const char *expected_name){char *name;int ret;BSTR bstr;IXMLDOMNode_get_nodeName(node, &bstr);name = convert_bstr_to_string(bstr);SysFreeString(bstr);ret = (strcmp(name, expected_name) == 0);free(name);return ret;}// 检查xml属性值是否为指定值int check_node_attr(IXMLDOMNode *node, const char *attr, const char *expected_value){char *value;int ret = 0;BSTR bstr;HRESULT hr;IXMLDOMElement *elem;VARIANT variant;IXMLDOMNode_QueryInterface(node, &IID_IXMLDOMElement, &elem); // 先把node转成elementbstr = convert_string_to_bstr(attr);hr = IXMLDOMElement_getAttribute(elem, bstr, &variant); // 再通过element获取属性值SysFreeString(bstr);if (SUCCEEDED(hr)){if (variant.vt == VT_BSTR){value = convert_bstr_to_string(variant.bstrVal);ret = (strcmp(value, expected_value) == 0);free(value);}else if (variant.vt == VT_NULL)ret = (expected_value == NULL || expected_value[0] == '\0');VariantClear(&variant);}IXMLDOMElement_Release(elem);return ret;}void display_xml(IXMLDOMDocument *xmldoc){char *text;long i, len;BSTR text_bstr;HRESULT hr;IXMLDOMElement *root;IXMLDOMNode *node;IXMLDOMNodeList *list;IXMLDOMDocument_get_documentElement(xmldoc, &root);display_attr(root, "timestr");display_attr(root, "example");display_attr(root, "title");hr = IXMLDOMElement_get_childNodes(root, &list);if (SUCCEEDED(hr)){IXMLDOMNodeList_get_length(list, &len);for (i = 0; i < len; i++){IXMLDOMNodeList_get_item(list, i, &node);if (check_node_name(node, "param") && check_node_attr(node, "method", "get") && check_node_attr(node, "name", "txt")){IXMLDOMNode_get_text(node, &text_bstr);text = convert_bstr_to_string(text_bstr);printf("txt: %s\n", text);free(text);SysFreeString(text_bstr);}else if (check_node_name(node, "param") && check_node_attr(node, "method", "post") && check_node_attr(node, "name", "hello")){IXMLDOMNode_get_text(node, &text_bstr);text = convert_bstr_to_string(text_bstr);printf("hello: %s\n", text);free(text);SysFreeString(text_bstr);}IXMLDOMNode_Release(node);}IXMLDOMNodeList_Release(list);}IXMLDOMElement_Release(root);}// 示例: 读取xml内容 (同步方式)void xml_test(const char *url, const char *post_data){long status;BSTR method_bstr, url_bstr, header_bstr, value_bstr;HRESULT hr;IDispatch *disp;IXMLDOMDocument *xmldoc;IXMLHttpRequest *xhr;VARIANT async;VARIANT body;VARIANT null;hr = CoCreateInstance(&CLSID_XMLHTTPRequest, NULL, CLSCTX_INPROC_SERVER, &IID_IXMLHttpRequest, &xhr);if (SUCCEEDED(hr)){if (post_data == NULL)method_bstr = convert_string_to_bstr("GET");elsemethod_bstr = convert_string_to_bstr("POST");printf("method: %ls\n", method_bstr); // 不带中文的BSTR可直接用%ls输出, 带中文就不行了url_bstr = convert_string_to_bstr(url);async.vt = VT_BOOL;async.boolVal = VARIANT_FALSE;null.vt = VT_NULL;hr = IXMLHttpRequest_open(xhr, method_bstr, url_bstr, async, null, null);SysFreeString(method_bstr);SysFreeString(url_bstr);if (SUCCEEDED(hr))printf("open() succeeded\n");if (post_data == NULL)body = null;else{header_bstr = convert_string_to_bstr("Content-Type");value_bstr = convert_string_to_bstr("application/x-www-form-urlencoded");IXMLHttpRequest_setRequestHeader(xhr, header_bstr, value_bstr);SysFreeString(header_bstr);SysFreeString(value_bstr);body.vt = VT_BSTR;body.bstrVal = convert_string_to_bstr(post_data);}hr = IXMLHttpRequest_send(xhr, body);if (SUCCEEDED(hr))printf("send() succeeded\n");VariantClear(&body);hr = IXMLHttpRequest_get_readyState(xhr, &status);if (SUCCEEDED(hr))printf("ready state: %d\n", status);hr = IXMLHttpRequest_get_status(xhr, &status);if (SUCCEEDED(hr))printf("http status code: %d\n", status);hr = IXMLHttpRequest_get_responseXML(xhr, &disp);if (SUCCEEDED(hr)){printf("get_responseXML() succeeded\n");hr = IDispatch_QueryInterface(disp, &IID_IXMLDOMDocument, &xmldoc);if (SUCCEEDED(hr)){printf("IDispatch_QueryInterface() succeeded\n");display_xmlstr(xmldoc);display_xml(xmldoc);IXMLDOMDocument_Release(xmldoc);}elseprintf("IDispatch_QueryInterface() failed\n");IDispatch_Release(disp);}elseprintf("get_responseXML() failed");IXMLHttpRequest_Release(xhr);}printf("\n");}int main(){CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); // 初始化COM组件对象模型str_test("http://adv.purasbar.com/mcu/test/str_test.php");str_test("http://adv.purasbar.com/mcu/test/json_test.php");xml_test("http://adv.purasbar.com/mcu/test/xml_test.php?txt=xp%E7%B3%BB%E7%BB%9F%E4%B8%8B%E8%BD%BD&yaya=%E5%96%9D%E5%AE%8C%E8%8D%AF%E6%84%9F%E8%A7%89%E6%B8%85%E7%88%BD%E4%BA%86%E8%AE%B8%E5%A4%9A", NULL); // get方式xml_test("http://adv.purasbar.com/mcu/test/xml_test.php?txt=xp%E7%B3%BB%E7%BB%9F%E4%B8%8B%E8%BD%BD", "hello=%E6%B5%8B%E8%AF%95%E7%8E%AF%E5%A2%83%E5%90%8E%E5%8F%B0%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F&yaya=%E9%9B%BE%E9%9B%A8%20%E8%BF%9E%E9%98%B4%E5%A4%A9%20%E7%BE%BD%E7%BE%BD%20%E6%B5%85%E6%B5%85%E5%81%A5%E5%81%A5%E5%BA%B7%E5%BA%B7%20%E9%BB%9B%E8%A1%8D%20%E9%9F%B3%E9%9F%B3%E9%94%A6%E9%B2%A4%E9%99%84"); // post方式CoUninitialize();return 0;}
get参数的提交方法:
将参数直接附加到URL网址的后面。如http://XXX//xml_test.php?A=B&C=D&E=F。汉字和符号必须用urlencode函数编码成%XX的形式。
post数据的提交方法:
通过VARIANT弱类型变量传入IXMLHttpRequest_send函数的第二个参数。没有post数据时要传入VT_NULL值。
汉字和符号同样也必须用urlencode函数编码成%XX的形式。只是最前面不需要以问号开头。
关于urlencode/urldecode、htmlspecialchars、trim和str_replace函数的C语言实现,请参考这篇文章:
https://blog.csdn.net/ZLK1214/article/details/131748124https://blog.csdn.net/ZLK1214/article/details/131748124
程序运行结果:
open() succeededsend() succeededready state: 4http status code: 200response: 当前时间为2024年1月24日 20:37:39。open() succeededsend() succeededready state: 4http status code: 200response: {"date":"2024-1-24 20:37:40","time":1706099860,"desc":"abcd\u7b80\u4f53\u4e2d\u6587"}method: GETopen() succeededsend() succeededready state: 4http status code: 200get_responseXML() succeededIDispatch_QueryInterface() succeededxmlstr: xp系统下载喝完药感觉清爽了许多timestr: 20240124203740example: 简体中文title: (null)txt: xp系统下载method: POSTopen() succeededsend() succeededready state: 4http status code: 200get_responseXML() succeededIDispatch_QueryInterface() succeededxmlstr: ktvtht5ebhkssu0k5srknmpic3xp系统下载测试环境后台管理系统雾雨 连阴天 羽羽 浅浅健健康康 黛衍 音音锦鲤附timestr: 20240124203740example: 简体中文title: (null)txt: xp系统下载hello: 测试环境后台管理系统请按任意键继续. . .
第三节 以异步方式发送Ajax请求
IXMLHttpRequest_open函数的六个参数分别是XMLHttpRequest对象、提交方式、URL网址、是否为异步方式、用户名和密码。
其中第四个参数若为VARIANT_FALSE,则是同步方式;若为VARIANT_TRUE,则是异步方式。
如果选择同步方式,则IXMLHttpRequest_send会阻塞,直到收到完整的服务器回应后,函数才返回。
如果选择异步方式,则IXMLHttpRequest_send不会阻塞,会立即返回。后面程序需要用一个while循环轮询IXMLHttpRequest_get_readyState,当ready_state=4时说明收到了完整的数据内容。
IXMLHttpRequest_get_readyState的值定义如下:
0: open函数还没调用
1: send函数还没调用
2: send函数调用了, get_status()函数已经可以使用了, 也可以读http headers了, 但是还读不了response
3: 已收到了部分数据, 可以读responseBody和responseText
4: 所有数据都已经接收完了, responseBody和responseText已经是完整数据了
IXMLHttpRequest_get_status函数返回的是HTTP的回应码,通常为200。如果找不到指定网页,就是400。如果遇到服务器端程序错误,就是500。
/* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展名为c */#define COBJMACROS#include #include #pragma comment(lib, "msxml6.lib")// char *转BSTR// 用完后调用SysFreeString释放BSTR convert_string_to_bstr(const char *s){int n;wchar_t *ws;BSTR bstr = NULL;n = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0);ws = malloc(n * sizeof(wchar_t));if (ws != NULL){MultiByteToWideChar(CP_ACP, 0, s, -1, ws, n);bstr = SysAllocString(ws);free(ws);}return bstr;}// BSTR转char *// 用完后调用free释放char *convert_bstr_to_string(BSTR bstr){char *s;int n;n = WideCharToMultiByte(CP_ACP, 0, bstr, -1, NULL, 0, NULL, NULL);s = malloc(n);if (s != NULL)WideCharToMultiByte(CP_ACP, 0, bstr, -1, s, n, NULL, NULL);return s;}// 去掉字符串末尾的\r\nvoid remove_last_crlf(char *str){int len;len = (int)strlen(str);if (len >= 2 && str[len - 2] == '\r' && str[len - 1] == '\n')str[len - 2] = '\0';}// 示例: 读取纯文本内容 (异步方式)// 请注意有两个名称不同但内容完全一样的接口: IXMLHttpRequest和IXMLHTTPRequest, 随便用哪个都可以// 但class id只有一个: CLSID_XMLHTTPRequestvoid str_test(const char *url){char *response;long ready_state, status;BSTR method_bstr, url_bstr, response_bstr;HRESULT hr;IXMLHttpRequest *xhr;VARIANT async; // VARIANT代表一个弱类型的变量VARIANT null;hr = CoCreateInstance(&CLSID_XMLHTTPRequest, NULL, CLSCTX_INPROC_SERVER, &IID_IXMLHttpRequest, &xhr);if (SUCCEEDED(hr)){// 打开连接// get参数内容是放到url字符串里面的method_bstr = convert_string_to_bstr("GET");url_bstr = convert_string_to_bstr(url);async.vt = VT_BOOL;async.boolVal = VARIANT_TRUE; // 选择异步方式 (选择这种方式后, main里面CoInitializeEx必须用COINIT_MULTITHREADED)null.vt = VT_NULL;hr = IXMLHttpRequest_open(xhr, method_bstr, url_bstr, async, null, null); // 最后两个参数是用户名和密码, 不用填SysFreeString(method_bstr);SysFreeString(url_bstr);if (SUCCEEDED(hr))printf("open() succeeded\n");// 发送请求// 由于我们选择的是异步方式, 所以send函数会直接返回hr = IXMLHttpRequest_send(xhr, null); // 第二个参数是post数据内容, 不用管if (SUCCEEDED(hr))printf("send() succeeded\n");while (1){// 获取请求状态// 0: open函数还没调用// 1: send函数还没调用// 2: send函数调用了, get_status()函数已经可以使用了, 也可以读http headers了, 但是还读不了response// 3: 已收到了部分数据, 可以读responseBody和responseText// 4: 所有数据都已经接收完了, responseBody和responseText已经是完整数据了hr = IXMLHttpRequest_get_readyState(xhr, &ready_state);if (SUCCEEDED(hr)){printf("ready state: %d\n", ready_state);if (ready_state == 4)break;}}// 获取http返回的response codehr = IXMLHttpRequest_get_status(xhr, &status);if (SUCCEEDED(hr))printf("http status code: %d\n", status);// 读取并显示文本内容hr = IXMLHttpRequest_get_responseText(xhr, &response_bstr);if (SUCCEEDED(hr)){// response_bstr是utf16编码, 转换后的response是gb2312编码// 把函数里面的CP_ACP改成CP_UTF8就可以转成utf8编码, 但是utf8就没办法用printf打印, 只能用fwrite写入txt文件再用记事本打开查看response = convert_bstr_to_string(response_bstr);remove_last_crlf(response);printf("response: %s\n", response);free(response);SysFreeString(response_bstr);}IXMLHttpRequest_Release(xhr);}printf("\n");}int main(){CoInitializeEx(NULL, COINIT_MULTITHREADED); // 初始化COM组件对象模型str_test("http://adv.purasbar.com/mcu/test/str_test.php");CoUninitialize();return 0;}
程序运行后,会不断地打印ready state=1,直到收到完整数据时ready state=4才退出循环,打印出收到的回应内容。
请注意,使用异步模式的前提是main函数里面CoInitializeEx的参数为COINIT_MULTITHREADED,不能是COINIT_APARTMENTTHREADED,否则readyState将一直为1,程序陷入死循环。
还有就是,COINIT_MULTITHREADED这个选项是不能在UI(图形界面)线程里面使用的,UI线程里面只能用COINIT_APARTMENTTHREADED。一般在主线程里面处理UI,单独新开一个线程处理网络通信。
第四节 直接通过IDispatch接口读取XML数据
IXMLHttpRequest_get_responseXML函数返回的是IDispatch接口,可以用IDispatch_QueryInterface函数将IDispatch转换成IXMLDOMDocument接口,然后读取XML数据。
IXMLDOMDocument其实是IDispatch的子类。实际上我们直接用IDispatch接口也可以读写xml数据。只不过要用wchar_t *字符串指定要调用的函数名称,用IDispatch_GetIDsOfNames函数查出函数名称对应的函数ID,用VARIANT数组(VARIANTARG是VARIANT的别名)指定函数参数后,再用IDispatch_Invoke函数通过函数ID和函数参数调用函数,并得到函数的返回值。
/* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展名为c */#define COBJMACROS#include #include #pragma comment(lib, "msxml6.lib")// char *转BSTR// 用完后调用SysFreeString释放BSTR convert_string_to_bstr(const char *s){int n;wchar_t *ws;BSTR bstr = NULL;n = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0);ws = malloc(n * sizeof(wchar_t));if (ws != NULL){MultiByteToWideChar(CP_ACP, 0, s, -1, ws, n);bstr = SysAllocString(ws);free(ws);}return bstr;}// BSTR转char *// 用完后调用free释放char *convert_bstr_to_string(BSTR bstr){char *s;int n;n = WideCharToMultiByte(CP_ACP, 0, bstr, -1, NULL, 0, NULL, NULL);s = malloc(n);if (s != NULL)WideCharToMultiByte(CP_ACP, 0, bstr, -1, s, n, NULL, NULL);return s;}void read_attribute_from_disp(IDispatch *disp, const char *attr){char *s;wchar_t *func_name;DISPID func_id;DISPPARAMS params = {0};HRESULT hr;VARIANT result;VARIANTARG arg;// 调用getAttribute方法 (带1个参数)func_name = L"getAttribute";hr = IDispatch_GetIDsOfNames(disp, &IID_NULL, &func_name, 1, LOCALE_SYSTEM_DEFAULT, &func_id);if (SUCCEEDED(hr)){arg.vt = VT_BSTR;arg.bstrVal = convert_string_to_bstr(attr);params.rgvarg = &arg;params.cArgs = 1;hr = IDispatch_Invoke(disp, func_id, &IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, ¶ms, &result, NULL, NULL);VariantClear(&arg);if (SUCCEEDED(hr)){if (result.vt == VT_BSTR){s = convert_bstr_to_string(result.bstrVal);printf("%ls: %s\n", func_name, s);free(s);}VariantClear(&result);}}}void read_xml_from_disp(IDispatch *disp){wchar_t *func_name;DISPID func_id;DISPPARAMS params = {0};HRESULT hr;VARIANT result;// 调用hasChildNodes方法func_name = L"hasChildNodes";hr = IDispatch_GetIDsOfNames(disp, &IID_NULL, &func_name, 1, LOCALE_SYSTEM_DEFAULT, &func_id);if (SUCCEEDED(hr)){hr = IDispatch_Invoke(disp, func_id, &IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, ¶ms, &result, NULL, NULL); // DISPATCH_METHOD指明是调用方法if (SUCCEEDED(hr)){if (result.vt == VT_BOOL){if (result.boolVal == VARIANT_TRUE)printf("%ls: true\n", func_name);else if (result.boolVal == VARIANT_FALSE)printf("%ls: false\n", func_name);}VariantClear(&result); // VARIANT类型的变量统一用这个函数释放}}// 读取documentElement属性func_name = L"documentElement";hr = IDispatch_GetIDsOfNames(disp, &IID_NULL, &func_name, 1, LOCALE_SYSTEM_DEFAULT, &func_id);if (SUCCEEDED(hr)){hr = IDispatch_Invoke(disp, func_id, &IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, ¶ms, &result, NULL, NULL); // DISPATCH_PROPERTYGET指明是读取属性if (SUCCEEDED(hr)){if (result.vt == VT_DISPATCH){read_attribute_from_disp(result.pdispVal, "timestr");read_attribute_from_disp(result.pdispVal, "example");}VariantClear(&result);}}}// 示例: 用IDispatch接口直接读取XML数据 (异步方式)void xml_test(const char *url){long ready_state;BSTR method_bstr, url_bstr;HRESULT hr;IDispatch *disp;IXMLHttpRequest *xhr;VARIANT async;VARIANT null;hr = CoCreateInstance(&CLSID_XMLHTTPRequest, NULL, CLSCTX_INPROC_SERVER, &IID_IXMLHttpRequest, &xhr);if (SUCCEEDED(hr)){method_bstr = convert_string_to_bstr("GET");url_bstr = convert_string_to_bstr(url);async.vt = VT_BOOL;async.boolVal = VARIANT_TRUE; // 选择异步方式null.vt = VT_NULL;IXMLHttpRequest_open(xhr, method_bstr, url_bstr, async, null, null);SysFreeString(method_bstr);SysFreeString(url_bstr);IXMLHttpRequest_send(xhr, null);while (1){hr = IXMLHttpRequest_get_readyState(xhr, &ready_state);if (SUCCEEDED(hr)){printf("ready state: %d\n", ready_state);if (ready_state == 4)break;}}hr = IXMLHttpRequest_get_responseXML(xhr, &disp);if (SUCCEEDED(hr)){read_xml_from_disp(disp);IDispatch_Release(disp);}IXMLHttpRequest_Release(xhr);}}int main(){CoInitializeEx(NULL, COINIT_MULTITHREADED); // 初始化COM组件对象模型xml_test("http://adv.purasbar.com/mcu/test/xml_test.php");CoUninitialize();return 0;}
在上面的程序中,我们直接用IDispatch_Invoke函数调用了hasChildNode函数判断了XML根节点下是否有子节点,获取了documentElement属性,然后还执行了documentElement.getAttribute()函数读取XML根节点上的两个属性值,属性名通过arg变量传入,属性值通过result变量传出,VARIANT弱类型变量使用完成后用VariantClear函数释放。
第五节 通过urlencode函数打包GET和POST数据
GET数据和POST数据采用的是键值对的形式,其中的汉字和特殊符号要用%XX编码,其中XX是UTF-8编码对应的十六进制数,通常一个汉字占三个字节。
当然也可以采用其他编码,如GB2312,但是我们的PHP服务器上用的是UTF-8编码。这个跟PHP代码有关。
C语言里面char *字符串是GB2312编码,先用MultiByteToWideChar转成UTF-16编码,再用WideCharToMultiByte转成UTF-8编码,最后用urlencode函数编码成%XX的形式。
键值对最终要写成A=B&C=D&E=F的形式,其中A、C、E是键名,B、D、F是它们对应的值,我们用create_param_string函数来完成这项工作。
#include #include #include #include struct param{char *name;char *value;char *reserved[4];};int urlencode_buf(char *buf, int *bufsize){char x;int i, j, len;if (buf == NULL){*bufsize = 1;return -1;}len = 0;for (i = 0; buf[i] != '\0'; i++){if (isalnum((unsigned char)buf[i]) || strchr(" -_.", buf[i]) != NULL)len++;elselen += 3;}if (*bufsize = i)*bufsize = len + 1;else*bufsize = i + 1;return -1;}j = len - 1;for (i--; i >= 0; i--){if (isalnum((unsigned char)buf[i]) || strchr("-_.", buf[i]) != NULL){buf[j] = buf[i];j--;}else if (buf[i] == ' '){buf[j] = '+';j--;}else{x = buf[i] & 15;buf[j] = (x 0){s[n] = '?';len++;}}for (i = 0; i < count; i++){if (i != 0){s[len] = '&';len++;}n = (int)strlen(params[i].reserved[1]);memcpy(s + len, params[i].reserved[1], n);len += n;if (params[i].reserved[3] != NULL){s[len] = '=';len++;n = (int)strlen(params[i].reserved[3]);memcpy(s + len, params[i].reserved[3], n);len += n;}}s[len] = '\0';assert(len == total);cleanup:for (i = 0; i < count; i++){for (j = 0; j < _countof(params[i].reserved); j++){if (params[i].reserved[j] != NULL){free(params[i].reserved[j]);params[i].reserved[j] = NULL;}}}return s;}int main(){char *str;char timestr[50];struct param params[4];struct tm tm;time_t t;params[0].name = "hello";params[0].value = "world";params[1].name = "yaya";params[1].value = "喝完药感觉清爽了许多";params[2].name = "test";params[2].value = NULL;params[3].name = "msg";params[3].value = timestr;time(&t);localtime_s(&tm, &t);strftime(timestr, sizeof(timestr), "当前时间: %Y-%m-%d %H:%M:%S", &tm);str = create_param_string("http://www.example.com/hello.php", params, _countof(params));if (str != NULL){printf("GET参数示例: %s\n", str);free(str);}str = create_param_string(NULL, params, _countof(params));if (str != NULL){printf("POST参数示例: %s\n", str);free(str);}return 0;}
程序运行结果如下。GET参数是附加到URL网址后面的,用问号隔开。POST数据是通过IXMLHttpRequest_send函数的第二个参数发给服务器的。
第六节 使用IXMLHttpRequest_put_onreadystatechange回调函数处理Ajax异步请求
当IXMLHttpRequest设置成异步模式后,数据还没有接收完,IXMLHttpRequest_send函数就会提前返回。这样我们就需要用一个while循环来轮询IXMLHttpRequest_get_readyState的值,当ready_state=4时才表明数据接收完了,这时才能读取response回应内容。
在刚才的程序运行截图里面可以看得出来,“ready state: 1”打印了很长一串,这期间CPU一直在空转,这说明while循环的执行时间还是很长的。
为了解决这个问题,我们可以使用IXMLHttpRequest_put_onreadystatechange函数设置一个回调函数,每当ready_state的值发生变化时IXMLHttpRequest自动帮我们调用这个回调函数,我们在这个回调函数里面去读取response回应。
IXMLHttpRequest_put_onreadystatechange函数有两个参数,第一个参数是IXMLHttpRequest对象,第二个参数是我们自己定义的包含回调函数的IDispatch对象。也就是说我们要自己实现一个IDispatch对象。
刚才在第四节里面已经解释了IDispatch到底是个什么东西。IDispatch是一个根据函数名字符串调用函数的工具。IXMLHttpRequest_get_responseXML函数是IXMLHttpRequest给我们提供IDispatch对象,而现在我们使用IXMLHttpRequest_put_onreadystatechange函数,就要自己实现一个IDispatch对象传递给IXMLHttpRequest。
C语言实现COM接口的方法是建立一个自定义的结构体,这个结构体的第一个成员变量是要实现的接口的函数指针表的指针,其他成员可随便自定义。
函数指针表的类型名称是在接口名称后面加上Vtbl。我们要实现的是IDispatch接口,所以函数指针表的类型名为IDispatchVtbl。
struct xxx
{
IDispatchVtbl *pvtbl;
…… (其他自定义成员)
};
pvtbl成员变量是一个指针,一定要指向一个变量,通常把指向的这个变量也放到结构体里面,或者单独malloc出来也行。
(方法1)
struct xxx
{
IDispatchVtbl *pvtbl;
IDispatchVtbl vtbl;
…… (其他自定义成员)
};
struct xxx x;
x.pvtbl = &x.vtbl;
(方法2)
struct xxx
{
IDispatchVtbl *pvtbl;
…… (其他自定义成员)
};
struct xxx x;
x.pvtbl = malloc(sizeof(IDispatchVtbl));
IDispatchVtbl也是一个结构体,里面一共有7个成员变量,全部为函数指针,这就是我们要实现的7个函数,如下表所示。
函数名 | 作用 |
---|---|
QueryInterface | 切换父类或子类 |
AddRef | 引用计数加1 |
Release | 引用计数减1 |
GetTypeInfoCount | (本例程用不到这个函数) |
GetTypeInfo | (本例程用不到这个函数) |
GetIDsOfNames | 获取函数名对应的函数ID (本例程用不到这个函数) |
Invoke | 执行指定ID号的函数 |
IDispatch接口继承IUnknown接口,QueryInterface、AddRef和Release这三个函数是父类IUnknown里面的函数,分别用于切换父子类、增加和减少引用计数。
通常我们定义的struct xxx结构体变量是由malloc动态分配出来的,引用计数的值什么时候为0决定了什么时候释放结构体变量所占用的内存。
struct xxx *p = malloc(sizeof(struct xxx));
然后就可以转换成IDispatch指针,并使用系统提供的IDispatch_XXX系列函数操作IDispatch对象。
IDispatch *disp = (IDispatch *)p;
IDispatch_XXX(disp, …);
当onreadystatechange事件发生时,IXMLHttpRequest会去调用我们的IDispatch对象的Invoke函数,传入的函数ID(dispIdMember)为0,参数个数(pDispParams->cArgs)也为0,函数也不需要返回值(pVarResult=NULL)。
我们来看一下例程。
main.c:
/* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展名为c */#define COBJMACROS#include #include #include "xhr_callback.h"#pragma comment(lib, "msxml6.lib")// char *转BSTR// 用完后调用SysFreeString释放BSTR convert_string_to_bstr(const char *s){int n;wchar_t *ws;BSTR bstr = NULL;n = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0);ws = malloc(n * sizeof(wchar_t));if (ws != NULL){MultiByteToWideChar(CP_ACP, 0, s, -1, ws, n);bstr = SysAllocString(ws);free(ws);}return bstr;}// BSTR转char *// 用完后调用free释放char *convert_bstr_to_string(BSTR bstr){char *s;int n;n = WideCharToMultiByte(CP_ACP, 0, bstr, -1, NULL, 0, NULL, NULL);s = malloc(n);if (s != NULL)WideCharToMultiByte(CP_ACP, 0, bstr, -1, s, n, NULL, NULL);return s;}// 去掉字符串末尾的\r\nvoid remove_last_crlf(char *str){int len;len = (int)strlen(str);if (len >= 2 && str[len - 2] == '\r' && str[len - 1] == '\n')str[len - 2] = '\0';}void test_callback(IXMLHttpRequest *xhr){char *response;long ready_state, status;BSTR response_bstr;HRESULT hr;// 获取请求状态hr = IXMLHttpRequest_get_readyState(xhr, &ready_state);if (SUCCEEDED(hr)){printf("ready_state: %d\n", ready_state);if (ready_state == 4){// 获取http返回的response codehr = IXMLHttpRequest_get_status(xhr, &status);if (SUCCEEDED(hr))printf("status: %d\n", status);// 读取并显示文本内容hr = IXMLHttpRequest_get_responseText(xhr, &response_bstr);if (SUCCEEDED(hr)){// response_bstr是utf16编码, 转换后的response是gb2312编码// 把函数里面的CP_ACP改成CP_UTF8就可以转成utf8编码, 但是utf8就没办法用printf打印, 只能用fwrite写入txt文件再用记事本打开查看response = convert_bstr_to_string(response_bstr);remove_last_crlf(response);printf("response text: %s\n", response);free(response);SysFreeString(response_bstr);}}}}void test(const char *url){BSTR method_bstr, url_bstr;HRESULT hr;IDispatch *xhr_callback_disp;IXMLHttpRequest *xhr;VARIANT async;VARIANT null;hr = CoCreateInstance(&CLSID_XMLHTTPRequest, NULL, CLSCTX_INPROC_SERVER, &IID_IXMLHttpRequest, &xhr);if (SUCCEEDED(hr)){// 打开连接method_bstr = convert_string_to_bstr("GET");url_bstr = convert_string_to_bstr(url);async.vt = VT_BOOL;async.boolVal = VARIANT_TRUE; // 选择异步方式null.vt = VT_NULL;IXMLHttpRequest_open(xhr, method_bstr, url_bstr, async, null, null);SysFreeString(method_bstr);SysFreeString(url_bstr);// 设置回调函数xhr_callback_disp = create_xhr_callback(xhr, test_callback);if (xhr_callback_disp != NULL){IXMLHttpRequest_put_onreadystatechange(xhr, xhr_callback_disp);IDispatch_Release(xhr_callback_disp);}// 发送请求IXMLHttpRequest_send(xhr, null);IXMLHttpRequest_Release(xhr);}}int main(){CoInitializeEx(NULL, COINIT_MULTITHREADED); // 初始化COM组件对象模型test("http://adv.purasbar.com/mcu/test/str_test.php");Sleep(5000);CoUninitialize();return 0;}
xhr_callback.h:
#pragma oncetypedef void (*xhr_callback)(IXMLHttpRequest *xhr);IDispatch *create_xhr_callback(IXMLHttpRequest *xhr, xhr_callback callback);
xhr_callback.c:
/* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展名为c */#define COBJMACROS#include #include #include "xhr_callback.h"// 实现IDispatch接口// IDispatch接口继承IUnknown接口struct xhr_callback{IDispatchVtbl *pvtbl;IDispatchVtbl vtbl;ULONG refcnt;xhr_callback callback;IXMLHttpRequest *xhr;};static HRESULT STDMETHODCALLTYPE xhr_callback_query_interface(IDispatch *This, REFIID riid, void **ppvObject){wchar_t *ws;HRESULT hr;hr = StringFromIID(riid, &ws);if (SUCCEEDED(hr)){printf("xhr_callback_query_interface: rrid=%ls\n", ws);CoTaskMemFree(ws);}elseprintf("xhr_callback_query_interface\n");if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IDispatch)){IDispatch_AddRef(This);*ppvObject = This;return S_OK;}else{*ppvObject = NULL;return E_NOINTERFACE;}}static ULONG STDMETHODCALLTYPE xhr_callback_addref(IDispatch *This){struct xhr_callback *p = (struct xhr_callback *)This;p->refcnt++;printf("xhr_callback_addref: %u\n", p->refcnt);return p->refcnt;}static ULONG STDMETHODCALLTYPE xhr_callback_release(IDispatch *This){struct xhr_callback *p = (struct xhr_callback *)This;p->refcnt--;printf("xhr_callback_release: %u\n", p->refcnt);if (p->refcnt == 0){IXMLHttpRequest_Release(p->xhr);memset(p, 0, sizeof(struct xhr_callback));free(p);return 0;}return p->refcnt;}static HRESULT STDMETHODCALLTYPE xhr_callback_get_type_info_count(IDispatch *This, UINT *pctinfo){printf("xhr_callback_get_type_info_count\n");*pctinfo = 0; // 不提供类型信息return S_OK;}static HRESULT STDMETHODCALLTYPE xhr_callback_get_type_info(IDispatch *This, UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo){printf("xhr_callback_get_type_info\n");*ppTInfo = NULL;return S_OK;}static HRESULT STDMETHODCALLTYPE xhr_callback_get_ids_of_names(IDispatch *This, REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId){UINT i;printf("xhr_callback_get_ids_of_names\n");for (i = 0; i cArgs, pVarResult);if (dispIdMember == 0 && pDispParams->cArgs == 0){if (p->callback != NULL)p->callback(p->xhr);}if (pVarResult != NULL)VariantInit(pVarResult);return S_OK;}IDispatch *create_xhr_callback(IXMLHttpRequest *xhr, xhr_callback callback){struct xhr_callback *p;IDispatch *disp;p = malloc(sizeof(struct xhr_callback));if (p == NULL)return NULL;memset(p, 0, sizeof(struct xhr_callback));p->pvtbl = &p->vtbl;p->pvtbl->QueryInterface = xhr_callback_query_interface;p->pvtbl->AddRef = xhr_callback_addref;p->pvtbl->Release = xhr_callback_release;p->pvtbl->GetTypeInfoCount = xhr_callback_get_type_info_count;p->pvtbl->GetTypeInfo = xhr_callback_get_type_info;p->pvtbl->GetIDsOfNames = xhr_callback_get_ids_of_names;p->pvtbl->Invoke = xhr_callback_invoke;p->callback = callback;IXMLHttpRequest_AddRef(xhr);p->xhr = xhr;disp = (IDispatch *)p;IDispatch_AddRef(disp);return disp;}
程序运行结果如下。
可以看到,只有当ready_state的值发生了变化,回调函数才会执行。回调函数总共就执行了四次,效率一下子提升了许多。在第四次执行回调函数的时候,ready_state=4,我们在test_callback回调函数中读取了IXMLHttpRequest_get_responseText回应内容并显示。