RESTful
REST(REpresentation State Transfer)描述了一个架构样式的网络系统,比如说web应用程序。它首次出现在2000年Roy Thomas Fielding的博士论文中,他是 HTTP 规范的主要编写者之一。REST指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计,即具有REST风格就是RESTful。在REST中,以资源为核心,任何事物,只要具有被引用的必要,就是一个资源。每个资源必须至少有一个统一资源标识符,即URI,URI既是资源的名称也是资源的地址。URI和资源之间的关系是多对一的,也就是说一个URI仅标识一个资源,但是一个资源可以有多余一个URI。REST中的资源是数据和表现形式的组合,以资源为核心的设计思想是REST的核心所在。资源是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的表现层(Representation)。如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State
Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。REST规范包括:客户-服务器,无状态,缓存,统一接口,分层系统和按需代码。这些架构约束是从Fielding的博士论文中直译过来的,所以极力推荐阅读Roy Thomas
Fielding的博士论文,可以对REST有一个更深刻的理解。
目前,基于RESTful的Web Service在一些领域得到了较好的利用,一些大公司已经提供了基于REST的网络服务。比如说的国外的谷歌、亚马逊等,国内的百度等公司。下面就以一个比较有意思的语音识别云服务为例,该服务是百度http://yuyin.baidu.com/提供的。
百度语音云服务
百度的语音识别服务通过RESTAPI的方式给开发者提供了一个通用的HTTP接口。这么做的一个好处就是轻量级。这里的轻量级的意思是,不需要在开发的应用中集成任何SDK,也不需要在测试机中添加任何的识别引擎。开发者只需要了解HTTP网络请求以及百度语音REST API的使用规则,就可以在自己的应用中实现语音识别功能。
当然,在使用百度的语音云服务之前,肯定是要进行开发者注册申请的,申请完以后百度会给开发者提供一个API Key和Secret Key,在后期跟百度的服务器通信时会用到两个Key。在使用百度的语音识别 REST API之前,需要获取一个Access Token。这个Access Token 是用户身份验证和授权的凭证。百度的语音识别采用的是Client Credentials(http://developer.baidu.com/wiki/index.php?title=docs/oauth/client)授权方式,即采用应用公钥、密钥的方式来获取Access
Token,适用于任何带server类型应用,通过此授权方式获取的Access Token仅可访问平台授权类的接口,也就是说,通过它所获取的Access Token只能用于访问与用户无关的Open API。获取Access Token的方法也很简单。开发者按照百度的要求,给百度的服务器发送特定格式的请求数据包,百度的服务器就会返回一个响应数据包,这个响应数据包里面就有Json格式的数据,其中的一个字段就是access_token。
获取Access Token需要应用给百度POST一个OAuth2.0授权服务的请求,POST到的地址为https://openapi.baidu.com/oauth/2.0/token,还需要带上以下的参数:grant_type:必须参数,固定为“client_credentials”;client_id:必须参数,也就是开发者申请时百度给开发者提供的API
Key;client_secret:必须参数,也就是开发者申请时百度给开发者提供的Secret Key;scope:非必须参数,是一个以空格分隔的权限列表。比如说一个标准的POST请求数据包的格式如下:
https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=Va5yQRHlA4Fq4eR3LT0vuXV4&client_secret= 0rDSjzQ20XUj5itV7WRtznPQSzr5pVw2
在请求参数无误的情况下,百度的服务器会返回一段Json格式的文本,具有以下字段:access_token:要获取的Access Token;expires_in:Access Token的有效期,以秒为单位;refresh_token:用于刷新Access Token 的Refresh Token,所有应用都会返回该参数;scope:Access Token最终的访问范围,即用户实际授予的权限列表(用户在授权页面时,有可能会取消掉某些请求的权限),关于权限的具体信息参考“权限列表”一节;session_key:基于http调用Open
API时所需要的Session Key,其有效期与Access Token一致;session_secret:基于http调用Open API时计算参数签名用的签名密钥。例如,百度服务器返回的一个响应数据包如下:
HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store { "access_token": "1.a6b7dbd428f731035f771b8d15063f61.86400.1292922000-2346678-124328", "expires_in": 86400, "refresh_token": "2.385d55f8615fdfd9edb7c4b5ebdc3e39.604800.1293440400-2346678-124328", "scope": "public", "session_key": "ANXxSNjwQDugf8615OnqeikRMu2bKaXCdlLxn", "session_secret": "248APxvxjCZ0VEC43EYrvxqaK4oZExMB", }
若请求错误,百度的服务器也会返回一段Json格式的文本,包含以下参数:error:错误码,即错误类型的代码;error_description:错误描述信息,用来帮助理解和解决发生的错误。比如说如下的请求响应数据包:
HTTP/1.1 400 Bad Request Content-Type: application/json Cache-Control: no-store { "error": "invalid_grant", "error_description":"Invalid authorization code: ANXxSNjwQDugOnqeikRMu2bKaXCdlLxn" }
想利用C或者C++语言来完成上述工作时,有一个很强大的工具:curl。curl是利用URL语法在命令行方式下工作的开源文件传输工具。它被广泛应用在Unix、多种Linux发行版中,并且有DOS和Win32、Win64下的移植版本。在C/C++中也有对应的开发库函数——libcurl,该库为开发者提供了丰富的库函数用于给服务器通信。比如说想利用curl获取百度的Access Token,方法也很简单,利用curl –s命令给百度的服务器POST一个请求就可以实现,我在自己的机器上实验如下图:
通过上图,可以看出响应的结果就是一段Json格式的数据包。其中 “access_token” 字段即为请求REST API 所需的令牌,默认情况下,Access Token的有效期是一个月,开发者需要对Access Token的有效性进行判断,如果Access Token过期可以重新获取。
想利用百度的语音识别服务来识别本地的一段语音也很简单,直接给百度的服务器POST自己的语音数据,对方的服务器就会返回一段Json格式的文本,相应的字段就是识别的结果,将这个结果解析出来就可以利用识别的结果了。
百度语音的云服务支持POST的方式来上传语音数据;目前百度语音的REST API仅支持整段语音识别的模式,即需要上传整段语音进行识别。语音数据的压缩格式有以下几种:pcm(不压缩)、wav、opus、speex、amr、x-flac这几种。
语音数据的上传方式有两种:隐示发送和显示发送。所谓的隐式发送就是就是将语音数据格式化成标准的Json格式数据,通过POST上传,这里的Json格式规定了相应的字段,当然,在格式化之前需要将语音数据进行Base64编码。所谓的显示发送顾名思义就是将语音数据直接放在HTTP-BODY中,控制REST参数以及相关的统计信息就可以通过REST API进行传递。对于上面的两种上传方式,百度的服务器都会返回统一的结果,都采用Json格式封装。如果识别成功,识别结果放在 JSON的“result”字段中,统一采用 utf-8
方式编码。
对于上面的所有HTTP动作,在liburl库中都有相应的库函数,所以用起来非常得方便。下面给出一个例子。利用百度的语音识别服务来POST一段本地pcm格式的语音数据给百度的服务器,语音数据的内容是“百度语音提供技术支持”。源码也很简单,主要就是base64编码、Json格式数据解析和利用libcurl库函数来进行POST、GET操作。环境是在Ubuntu下进行的,当然在使用之前需要先安装jsoncpp库和libcurl库,安装方法也很简单,网上有一大堆。这里直接贴上源码:
#include <cstdio> #include <cstring> #include <stdlib.h> #include "curl/curl.h" #include "curl/easy.h" #include "json/json.h" #define MAX_BUFFER_SIZE 512 #define MAX_BODY_SIZE 1000000 using namespace std; static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; static inline bool is_base64(unsigned char c) { return (isalnum(c) || (c == '+') || (c == '/')); } string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { std::string ret; int i = 0; int j = 0; unsigned char char_array_3[3]; unsigned char char_array_4[4]; while (in_len--) { char_array_3[i++] = *(bytes_to_encode++); if (i == 3) { char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for (i = 0; (i <4); i++) ret += base64_chars[char_array_4[i]]; i = 0; } } if (i) { for (j = i; j < 3; j++) char_array_3[j] = '\0'; char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; while ((i++ < 3)) ret += '='; } return ret; } string base64_decode(std::string const& encoded_string) { int in_len = encoded_string.size(); int i = 0; int j = 0; int in_ = 0; unsigned char char_array_4[4], char_array_3[3]; std::string ret; while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i <4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j <4; j++) char_array_4[j] = 0; for (j = 0; j <4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } //回调函数 static size_t writefunc(void *ptr, size_t size, size_t nmemb, char **result) { size_t result_len = size * nmemb; *result = (char *)realloc(*result, result_len + 1); if (*result == NULL) { printf("realloc failure!\n"); return 1; } memcpy(*result, ptr, result_len); (*result)[result_len] = '\0'; cout<<百度服务器返回的json数据:"<<*result<<endl; /*Json::Reader reader; Json::Value root; if(reader.parse(result,root)) { string res = root["result"].asString(); cout <<"解析的结果: "<< res << endl; }*/ return result_len; } int main() { freopen("out.txt", "w", stdout); int json_file_size; FILE *pFile = NULL; char *audio_data; pFile = fopen("test.pcm", "r"); if (pFile == NULL) { perror("Open file error!\n"); } else { fseek(pFile, 0, SEEK_END); int file_size = ftell(pFile); cout << "file size: " << file_size << " bytes" << endl; fseek(pFile, 0, SEEK_SET); audio_data = (char *)malloc(sizeof(char)*file_size); fread(audio_data, file_size, sizeof(char), pFile); //机器的mac地址 char *cuid = "56:84:7a:fe:97:99"; char *api_key = "6yFhYifMjXc8QmubiICXBQgi"; char *secret_key = "nZn45o3X0LGx42qovumYy2mjpOiOup2E"; char host[MAX_BUFFER_SIZE]; snprintf(host, sizeof(host), "https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", api_key, secret_key); cout << "curl -s命令的host: " << host << endl; FILE *p = NULL; char cmd[MAX_BUFFER_SIZE]; //curl -s命令的返回结果 char *result = (char*)malloc(MAX_BUFFER_SIZE); char *curl_cmd = "curl -s "; char *yinhao = "\""; strcpy(cmd, curl_cmd); strcat(cmd, yinhao); strcat(cmd, host); strcat(cmd, yinhao); p = popen(cmd, "r"); fgets(result, MAX_BUFFER_SIZE, p); cout << "curl -s 响应结果: " << result << endl; pclose(p); string access_token; //解析服务器返回的Json数据,获取access_token if (result != NULL) { Json::Reader reader; Json::Value root; if (reader.parse(result, root, false)) { access_token = root.get("access_token", "").asString(); } cout << "access_token: " << access_token << endl; } //采取隐式发送的方式给服务器发送json格式的数据 char body[MAX_BODY_SIZE]; memset(body, 0, sizeof(body)); string decode_data = base64_encode((const unsigned char *)audio_data, file_size); if (0 == decode_data.length()) { cout << "Error!base64 encoded data is empty!"; return 1; } else { Json::Value buffer; Json::FastWriter buf_writer; buffer["format"] = "pcm"; buffer["rate"] = 8000; buffer["channel"] = 1; buffer["token"] = access_token.c_str(); buffer["cuid"] = cuid; buffer["speech"] = decode_data; buffer["len"] = file_size; //实际json格式数据的长度 json_file_size = buf_writer.write(buffer).length(); cout << "Json file size:" << json_file_size << " bytes" << endl; memcpy(body, buf_writer.write(buffer).c_str(), json_file_size); CURL *curl; CURLcode res;//服务器的响应结果 char *result_buffer = NULL; struct curl_slist *http_header = NULL; char temp[MAX_BUFFER_SIZE]; memset(temp, 0, sizeof(temp)); snprintf(temp, sizeof(temp), "%s", "Content-Type: application/json; charset=utf-8"); http_header = curl_slist_append(http_header, temp); snprintf(temp, sizeof(temp), "Content-Length: %d", json_file_size); http_header = curl_slist_append(http_header, temp); memset(host, 0, sizeof(host)); snprintf(host, sizeof(host), "%s", "http://vop.baidu.com/server_api"); cout << "server host: " << host << endl; curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, host);//设置访问的URL curl_easy_setopt(curl, CURLOPT_POST, 1);//1表示常规的http post请求 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30);//设置延时 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, http_header); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, json_file_size); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);//设置回调函数 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result_buffer); res = curl_easy_perform(curl); if (res != CURLE_OK) { printf("perform curl error:%d.\n", res); return 1; } curl_slist_free_all(http_header); curl_easy_cleanup(curl); free(audio_data); } } fclose(pFile); return 0; }
因为利用了额外的库函数,所以在编译时需要加上链接参数,编译命令如下:
g++-o bdvoice BaiduVR.cpp -L . -l json_linux-gcc-4.8_libmt -l curl
注:这里将jsoncpp库的头文件在json文件夹下和libcurl库的头文件在curl文件夹下,以及相应的.a和.so库跟代码文件放在一个目录下。运行可执行文件,源码中将标准输出写入到out.txt,out.txt的内容如下: