要实现一个web server,需要了解基本的Http协议。
tars内置了对http协议的支持,使用还是比较简单的。
它复用了 tars rpc,因此也拥有了同步和异步以及超时的特性,并能够利用tars stat上报调用质量。
官方文档:https://github.com/TarsCloud/TarsDocs/blob/master/dev/tarscpp/tars-http1.md
本文结合笔者开发过程作一记录,并对开发中遇到的问题进行详细描述。
服务框架的生成与普通的tars协议一致。官方文档及demo已经描写清楚,不再详述。
对于只对外提供Http服务的情况,只需要在初始化时增加以下代码:
void HttpServer::initialize()
{
addServant<HttpImp>(ServerConfig::Application + "." + ServerConfig::ServerName + ".HttpObj");
addServantProtocol(ServerConfig::Application + "." + ServerConfig::ServerName + ".HttpObj", &TC_NetWorkBuffer::parseHttp);
}
然后在Imp中实现具体业务逻辑:
int HttpImp::doRequest(TarsCurrentPtr current, vector<char> &buffer)
{
// parse request header
TC_HttpRequest request; // util中tc_http.h中提供了操作HTTP的工具类
vector<char> v = current->getRequestBuffer();
string sBuf;
sBuf.assign(&v[0],v.size());
request.decode(sBuf);
// 构建响应包
TC_HttpResponse rsp; // 可以为rsp设置属性
string s="hello";
rsp.setResponse(s.c_str(),s.size());
rsp.encode(buffer);
return 0;
}
以上示例代码完成了srv的开发,部署的时候选择非TARS协议,使用浏览器访问该端口,就会收到hello的问候。
client的使用流程同其他协议。
先获取代理,并需要设置http编码函数:
CommunicatorPtr& comm = Application::getCommunicator();
ServantPrx prx = comm->stringToProxy<ServantPrx>("test.server.HttpObj"); // 根据实际名称填写
// set protocol
prx->tars_set_protocol(ServantProxy::PROTOCOL_HTTP1, 10); // 设置协议为HTTP1,并设置串行连接个数为10
虽然http支持长短连接, 但是http无法连接复用, 即一条连接上只能并行跑一个包(请求和响应), 因此tars rpc专门支持了串行模式:
prx->tars_connection_serial(10)
相当于启动了10个http连接, 在连接上完成http请求。作用同上述代码中的参数。
同步调用:
shared_ptr<TC_HttpResponse> rsp;
shared_ptr<TC_HttpRequest> req = std::make_shared<TC_HttpRequest>();
req->setPostRequest("http://domain.com/hello", string("helloworld"), true);
//可以设置http头支持, keep-alive
req->setHeader("Connection", "keep-alive"); // 尽量使用提供的函数设置头属性,不要直接设置
prx->http_call("hello", req, rsp); // 这里http_call第一个参数没有实际函数, 只是用来监控用, 即在web管理平台接口统计看到函数名是"hello"
异步调用:
// 编写callback
class MyHttpCb : public HttpCallback
{
public:
virtual int onHttpResponse(const shared_ptr<TC_HttpResponse> &rsp)
{
return 0;
}
virtual int onHttpResponseException(int expCode)
{
return 0;
}
};
// 发起调用
shared_ptr<TC_HttpRequest> req = std::make_shared<TC_HttpRequest>();
string buff = "helloworld";
req->setPostRequest("http://domain.com/hello", buff, true);
prx->http_call_async("hello", req, p);
可以看到,对于熟悉TARS开发的来说,这个流程还是挺简单的。
一般业务交互过程中,request会在url中表达需求,而服务端根据url处理请求。
通过TC_HttpRequest
的getRequestUrl
获取url即可。
实际使用中,不会像示例中那么简单,经常需要使用Cookie、Token等来实现业务。
关于cookie的更多信息,请参考HTTP cookies。
在进行编码前,建议仔细阅读tc_http.h头文件,熟悉提供的各种接口。
使用getCookie()
获取cookie。
服务端需要把token相关信息传输给浏览器,浏览器每次请求页面的时候都会把token带过来。这样服务器就能通过token来验证请求的合法性。
使用 TC_HttpResponse
的setSetCookie
设置Cookie:
string gmt = TC_Common::tm2GMTstr(time(NULL) + 8 * 60 * 60 + 1 * 60 * 60); // 设置超期时间为1小时
rsp.setSetCookie("id=0; Path=/; Domain=qq.com; Expires=" + gmt); // Path/Domain/Expires等属性直接写在后面,注意Expires需要设置为GMT时间
rsp.setSetCookie("name=foo; Path=/; Domain=qq.com; Expires=" + gmt); // 可以设置Cookie的多个域
rsp.setCacheControl("max-age=3600"); // 也可以这样设置Cookie的超时时间
关于cacheControl的更多信息,请参考Cache-Control。
关于Max-age与Expires的区别,请参考HTTP Cookies: What’s the difference between Max-age and Expires?。
简单来说:
也可以直接设置:
// test_tc_http.cpp
string gmt = TC_Common::tm2GMTstr(time(NULL) + 10);
string s = "HTTP/1.1 200 OK\r\n";// 200 Aouut Error\r\n";
s += "Set-Cookie: n1=1; a=1; c=d; Path=/; Domain=qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n2=2; a=0; c=d; Path=/abc/def; Domain=.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n3=3; a=5; c=d; Path=/abc/def/aaa; Domain=.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n4=4; a=6; c=d; Path=/abc; Domain=.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n5=5; a=2; c=d; Path=/; Domain=.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n6=6; c=3; Path=/; Domain=y.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n7=7; c=3; Path=/abc; Domain=.y.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n8=6; c=3; Path=/; Domain=x.y.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n9=7; c=4; Path=/; Domain=.x.y.qq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n10=7; c=4; Path=/; Domain=qqq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n11=7; c=4; Path=/; Domain=.qqq.com; Expires=" + gmt + "\r\n";
s += "Set-Cookie: n12=8; c=4; Expires=" + gmt + "\r\n";
s += "Accept-Ranges: bytes\r\n\r\n";
TC_HttpResponse rsp;
rsp.decode s);
上节已经描述了Cookie及其属性的使用,这里记录一下我遇到的问题。
现象:
定位:
原因:
修改:
如果对Http协议熟悉,这个问题其实很简单,可以说基本上不应该犯这样的错误。
所以磨刀不误砍柴工,在做的过程中学习,在学习中思考,在思考中总结,这样才能越做越好。
作为服务端,在接收到req后,如果后台需要异步调用其他服务后,才能返回结果,就要使用异步返回rsp的方式了。
如图所示,在 web srver 接收到req后,需要异步调用srv1的tars服务,由于是异步调用,所以它不会立即返回。
web srv就需要在srv1异步返回后才能返回给web。
如果两边都是TARS服务,异步嵌套调用方法如下:
// 中间模块为proxy, 它异步调用Hello后,才返回结果给请求端
//ProxyServer中的异步回调对象
class HelloCallback : public HelloPrxCallback
{
public:
HelloCallback(TarsCurrentPtr ¤t) // 回调对象HelloCallback保存了上下文current
: _current(current)
{}
virtual void callback_testHello(tars::Int32 ret, const std::string& sOut)
{
Proxy::async_response_testProxy(_current, ret, sOut); // 回调对象收到HelloServer返回的请求以后,通过Proxy::async_response_testProxy再将请求返回client
}
virtual void callback_testHello_exception(tars::Int32 ret)
{
TLOGERROR("HelloCallback callback_testHello_exception ret:" << ret << endl);
Proxy::async_response_testProxy(_current, ret, "");
}
TarsCurrentPtr _current;
};
//定义ProxyServer中接口
tars::Int32 ProxyImp::testProxy(const std::string& sIn, std::string &sOut, tars::TarsCurrentPtr current)
{
try
{
current->setResponse(false); // 在ProxyServer的接口testProxy实现中,需要设置不自动回复current->setResponse(false)
// 回调对象必须是new出来放在智能指针中,例如:HelloPrxCallbackPtr cb = new HelloCallback(current); 它的生命周期,业务不用管理,框架层会自动管理
TestApp::HelloPrxCallbackPtr cb = new HelloCallback(current); // 如果回调对象中需要其他参数,可以在构造的时候传进去
_prx->tars_set_timeout(3000)->async_testHello(cb,sIn);
}
catch(std::exception &ex)
{
current->setResponse(true);
TLOGERROR("ProxyImp::testProxy ex:" << ex.what() << endl);
}
return 0; // 这里testProxy的返回值,返回参数其实都没有意义了,无论返回什么都可以
}
同TARS服务一样,web server作为代理,也具有异步返回的功能:
// 在代理模块中设置不自动回复
current->setResponse(false); // 直接使用current指针完成
// 异步调用其他服务
DemoCBPtr cb = new DemoCB(rsp, buffer, current); // 需要把rsp和buffer传给回调类
cb->async_HelloReq();
// 在异步调用的返回模块里执行异步rsp
// rsp填充
rsp.setResponse("Hello");
rsp.encode(buffer);
current_->sendResponse(buffer.data(), buffer.size()); // 返回给req
可以看到,异步嵌套调用的流程与TARS协议一致。
总体而言,TARS对Http协议的支持还是很好的,使用也很简单。
对于使用方法,由于文档描述不多,demo示例较简单,建议参考test_tc_http.cpp
,很详细的使用。
由于本人仍在学习中,文中错漏之处请指正。