这篇文章记录 Lite2D 里 HttpClient 的实现思路。网络请求底层封装的是 curl,由于网络部分运行在独立线程,所以需要和主线程之间做线程同步。
Director 的主线程同步
为了让其他线程能够安全地把回调派发回主线程执行,Director 维护了一个互斥锁保护下的函数队列。其他线程通过 addFuncToMainThread 把 lambda 放入队列,主线程每一帧调用 processOtherThreadFunc 统一取出并执行,执行完就清空队列。
void Director::addFuncToMainThread(const std::function<void()>& func)
{
_mutex_mainThread.lock();
_queue_other_thread_func.push_back(func);
_mutex_mainThread.unlock();
}
void Director::processOtherThreadFunc()
{
// for (int i = 0;i<_queue_other_thread_func.size(); ++i)
// {
// _queue_other_thread_func[i]();
// }
_mutex_mainThread.lock();
for (int i = 0; i < _queue_other_thread_func.size(); ++i)
{
_queue_other_thread_func[i]();
}
_queue_other_thread_func.clear();
_mutex_mainThread.unlock();
}
Lite2D 的 HttpClient 实现
HttpClient 是一个单例,内部起一个独立工作线程跑 workFunc。外部通过 send 把 HttpRequest 投递进请求队列,工作线程被条件变量唤醒后从队列取出请求,调用 doRequest_curl 实际执行 curl。
curl 的数据回调 write_data 会把响应写入 HttpRespone,然后通过 Director::addFuncToMainThread 把用户的 HttpCallBack 派发回主线程执行,避免在网络线程里直接触发游戏逻辑。
doRequest_curl 里还根据 curl 返回的 HTTP 状态码(200 / 0 / 其他)分别设置 HttpStatus 为 OK / TIMEOUT / NETERROR,其中超时分支会再次手动调用一次 write_data,以便把超时事件也通过同样的回调链路回调出去。
#include"HttpClient.h"
#include "HttpRequest.h"
#include "HttpRespone.h"
#include "../base/Director.h"
#include "../3party/curl/curl.h"
static int write_data(void *buffer, size_t size, size_t buffer_size, void *_respone)
{
HttpRespone *respone = (HttpRespone*)_respone;
HttpRequest*request = respone->getHttpRequest();
respone->writeData(buffer, buffer_size);
request->release();
Director::getInstance()->addFuncToMainThread([=]()
{
const HttpCallBack & x = request->getHttpCallback();
x(request, respone);
request->release();
respone->release();
});
return 0;
}
HttpClient* HttpClient::getInstance()
{
static HttpClient*ins = nullptr;
if (ins == nullptr)
{
ins = new HttpClient;
}
return ins;
}
void HttpClient::doRequest_curl(HttpRequest*request)
{
HttpRespone *respone = HttpRespone::create();
CURL*easy_handle = curl_easy_init();
curl_easy_setopt(easy_handle, CURLOPT_URL, request->getUrl().c_str());
curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data);
curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, respone);
curl_easy_setopt(easy_handle, CURLOPT_TIMEOUT, this->_time_read);
curl_easy_setopt(easy_handle, CURLOPT_CONNECTTIMEOUT, this->_time_connect);
// do post or get
if (request->getHttpType() == HttpType::POST)
{//post
curl_easy_setopt(easy_handle, CURLOPT_HTTPPOST, 1);
curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, request->getPostData().c_str());
}
else if (request->getHttpType() == HttpType::GET)
{//get
curl_easy_setopt(easy_handle, CURLOPT_HTTPGET, 1);
}
respone->setHttpRequest(request);
request->setCURLhandle(easy_handle);
// perform
auto succ = curl_easy_perform(easy_handle);
// do http status code 404 200 etc.
int code;
curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &code);
std::cout << " code:" << code << std::endl;
switch (code)
{
case 200:
{
respone->setHttpStatus(HttpStatus::OK);
}break;
case 0:
{//timeout such read and connect
respone->setHttpStatus(HttpStatus::TIMEOUT);
write_data(0, 0, 0, respone);
}break;
default:
{//404 etc.
respone->setHttpStatus(HttpStatus::NETERROR);
}
break;
}
respone->setOriginHttpCode(code);
}
void HttpClient::workFunc()
{
while (true)
{
HttpRequest* request;
_mutex.lock();
if (_queue_request.size() <= 0)
{
_condition.wait(_mutex);
}
request = _queue_request.front();
_queue_request.pop();
_mutex.unlock();
// log("net thread %d", _queue_request.size());
this->doRequest_curl(request);
}
}
HttpClient::HttpClient()
{
this->_time_connect = 5;
this->_time_read = 10;
curl_global_init(CURL_GLOBAL_WIN32);
auto t = std::thread([=]
{
this->workFunc();
});
t.detach();
}
void HttpClient::send(HttpRequest*request)
{
request->retain();
_mutex.lock();
_queue_request.push(request);
_mutex.unlock();
_condition.notify_one();
}
HttpClient::~HttpClient()
{
_condition.notify_all();
}