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的内容如下: