no message
This commit is contained in:
781
cocos/network/Downloader-curl.cpp
Normal file
781
cocos/network/Downloader-curl.cpp
Normal file
@@ -0,0 +1,781 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/Downloader-curl.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/StringUtil.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "base/std/container/deque.h"
|
||||
#include "base/std/container/set.h"
|
||||
#include "base/std/container/vector.h"
|
||||
#include "network/Downloader.h"
|
||||
#include "platform/FileUtils.h"
|
||||
|
||||
// **NOTE**
|
||||
// In the file:
|
||||
// member function with suffix "Proc" designed called in DownloaderCURL::_threadProc
|
||||
// member function without suffix designed called in main thread
|
||||
|
||||
#ifndef CC_CURL_POLL_TIMEOUT_MS
|
||||
#define CC_CURL_POLL_TIMEOUT_MS 50
|
||||
#endif
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation DownloadTaskCURL
|
||||
|
||||
class DownloadTaskCURL : public IDownloadTask {
|
||||
static int _sSerialId;
|
||||
|
||||
// if more than one task write to one file, cause file broken
|
||||
// so use a set to check this situation
|
||||
static ccstd::set<ccstd::string> _sStoragePathSet;
|
||||
|
||||
public:
|
||||
int serialId;
|
||||
|
||||
DownloadTaskCURL()
|
||||
: serialId(_sSerialId++),
|
||||
_fp(nullptr) {
|
||||
_initInternal();
|
||||
DLLOG("Construct DownloadTaskCURL %p", this);
|
||||
}
|
||||
|
||||
virtual ~DownloadTaskCURL() {
|
||||
// if task destroyed unnormally, we should release WritenFileName stored in set.
|
||||
// Normally, this action should done when task finished.
|
||||
if (_tempFileName.length() && _sStoragePathSet.end() != _sStoragePathSet.find(_tempFileName)) {
|
||||
DownloadTaskCURL::_sStoragePathSet.erase(_tempFileName);
|
||||
}
|
||||
if (_fp) {
|
||||
fclose(_fp);
|
||||
_fp = nullptr;
|
||||
}
|
||||
DLLOG("Destruct DownloadTaskCURL %p", this);
|
||||
}
|
||||
|
||||
bool init(const ccstd::string &filename, const ccstd::string &tempSuffix) {
|
||||
if (0 == filename.length()) {
|
||||
// data task
|
||||
_buf.reserve(CURL_MAX_WRITE_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// file task
|
||||
_fileName = filename;
|
||||
_tempFileName = filename;
|
||||
_tempFileName.append(tempSuffix);
|
||||
|
||||
if (_sStoragePathSet.end() != _sStoragePathSet.find(_tempFileName)) {
|
||||
// there is another task uses this storage path
|
||||
_errCode = DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
_errCodeInternal = 0;
|
||||
_errDescription = "More than one download file task write to same file:";
|
||||
_errDescription.append(_tempFileName);
|
||||
return false;
|
||||
}
|
||||
_sStoragePathSet.insert(_tempFileName);
|
||||
|
||||
// open temp file handle for write
|
||||
bool ret = false;
|
||||
do {
|
||||
ccstd::string dir;
|
||||
size_t found = _tempFileName.find_last_of("/\\");
|
||||
if (found == ccstd::string::npos) {
|
||||
_errCode = DownloadTask::ERROR_INVALID_PARAMS;
|
||||
_errCodeInternal = 0;
|
||||
_errDescription = "Can't find dirname in storagePath.";
|
||||
break;
|
||||
}
|
||||
|
||||
// ensure directory is exist
|
||||
auto util = FileUtils::getInstance();
|
||||
dir = _tempFileName.substr(0, found + 1);
|
||||
if (false == util->isDirectoryExist(dir)) {
|
||||
if (false == util->createDirectory(dir)) {
|
||||
_errCode = DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
_errCodeInternal = 0;
|
||||
_errDescription = "Can't create dir:";
|
||||
_errDescription.append(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// open file
|
||||
_fp = fopen(util->getSuitableFOpen(_tempFileName).c_str(), "ab");
|
||||
if (nullptr == _fp) {
|
||||
_errCode = DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
_errCodeInternal = 0;
|
||||
_errDescription = "Can't open file:";
|
||||
_errDescription.append(_tempFileName);
|
||||
}
|
||||
ret = true;
|
||||
} while (0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void initProc() {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_initInternal();
|
||||
}
|
||||
|
||||
void setErrorProc(int code, int codeInternal, const char *desc) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_errCode = code;
|
||||
_errCodeInternal = codeInternal;
|
||||
_errDescription = desc;
|
||||
}
|
||||
|
||||
size_t writeDataProc(unsigned char *buffer, size_t size, size_t count) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
size_t ret = 0;
|
||||
if (_fp) {
|
||||
ret = fwrite(buffer, size, count, _fp);
|
||||
} else {
|
||||
ret = size * count;
|
||||
auto cap = _buf.capacity();
|
||||
auto bufSize = _buf.size();
|
||||
if (cap < bufSize + ret) {
|
||||
_buf.reserve(bufSize * 2);
|
||||
}
|
||||
_buf.insert(_buf.end(), buffer, buffer + ret);
|
||||
}
|
||||
if (ret) {
|
||||
_bytesReceived += ret;
|
||||
_totalBytesReceived += ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class DownloaderCURL;
|
||||
|
||||
// for lock object instance
|
||||
std::mutex _mutex;
|
||||
|
||||
// header info
|
||||
bool _acceptRanges;
|
||||
bool _headerAchieved;
|
||||
uint32_t _totalBytesExpected;
|
||||
|
||||
ccstd::string _header; // temp buffer for receive header string, only used in thread proc
|
||||
|
||||
// progress
|
||||
uint32_t _bytesReceived;
|
||||
uint32_t _totalBytesReceived;
|
||||
|
||||
// error
|
||||
int _errCode;
|
||||
int _errCodeInternal;
|
||||
ccstd::string _errDescription;
|
||||
|
||||
// for saving data
|
||||
ccstd::string _fileName;
|
||||
ccstd::string _tempFileName;
|
||||
ccstd::vector<unsigned char> _buf;
|
||||
FILE *_fp;
|
||||
|
||||
void _initInternal() {
|
||||
_acceptRanges = (false);
|
||||
_headerAchieved = (false);
|
||||
_bytesReceived = (0);
|
||||
_totalBytesReceived = (0);
|
||||
_totalBytesExpected = (0);
|
||||
_errCode = (DownloadTask::ERROR_NO_ERROR);
|
||||
_errCodeInternal = (CURLE_OK);
|
||||
_header.resize(0);
|
||||
_header.reserve(384); // pre alloc header string buffer
|
||||
}
|
||||
};
|
||||
int DownloadTaskCURL::_sSerialId;
|
||||
ccstd::set<ccstd::string> DownloadTaskCURL::_sStoragePathSet;
|
||||
|
||||
typedef std::pair<std::shared_ptr<const DownloadTask>, DownloadTaskCURL *> TaskWrapper;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation DownloaderCURL::Impl
|
||||
// This class shared by DownloaderCURL and work thread.
|
||||
class DownloaderCURL::Impl : public std::enable_shared_from_this<DownloaderCURL::Impl> {
|
||||
public:
|
||||
DownloaderHints hints;
|
||||
|
||||
Impl()
|
||||
// : _thread(nullptr)
|
||||
{
|
||||
DLLOG("Construct DownloaderCURL::Impl %p", this);
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
DLLOG("Destruct DownloaderCURL::Impl %p %d", this, _thread.joinable());
|
||||
}
|
||||
|
||||
void addTask(std::shared_ptr<const DownloadTask> task, DownloadTaskCURL *coTask) {
|
||||
if (DownloadTask::ERROR_NO_ERROR == coTask->_errCode) {
|
||||
std::lock_guard<std::mutex> lock(_requestMutex);
|
||||
_requestQueue.push_back(make_pair(task, coTask));
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(_finishedMutex);
|
||||
_finishedQueue.push_back(make_pair(task, coTask));
|
||||
}
|
||||
}
|
||||
|
||||
void run() {
|
||||
std::lock_guard<std::mutex> lock(_threadMutex);
|
||||
if (false == _thread.joinable()) {
|
||||
std::thread newThread(&DownloaderCURL::Impl::_threadProc, this);
|
||||
_thread.swap(newThread);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard<std::mutex> lock(_threadMutex);
|
||||
if (_thread.joinable()) {
|
||||
_thread.detach();
|
||||
}
|
||||
}
|
||||
|
||||
bool stoped() {
|
||||
std::lock_guard<std::mutex> lock(_threadMutex);
|
||||
return false == _thread.joinable() ? true : false;
|
||||
}
|
||||
|
||||
void getProcessTasks(ccstd::vector<TaskWrapper> &outList) {
|
||||
std::lock_guard<std::mutex> lock(_processMutex);
|
||||
outList.reserve(_processSet.size());
|
||||
outList.insert(outList.end(), _processSet.begin(), _processSet.end());
|
||||
}
|
||||
|
||||
void getFinishedTasks(ccstd::vector<TaskWrapper> &outList) {
|
||||
std::lock_guard<std::mutex> lock(_finishedMutex);
|
||||
outList.reserve(_finishedQueue.size());
|
||||
outList.insert(outList.end(), _finishedQueue.begin(), _finishedQueue.end());
|
||||
_finishedQueue.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
static size_t _outputHeaderCallbackProc(void *buffer, size_t size, size_t count, void *userdata) {
|
||||
int strLen = int(size * count);
|
||||
DLLOG(" _outputHeaderCallbackProc: %.*s", strLen, buffer);
|
||||
DownloadTaskCURL &coTask = *((DownloadTaskCURL *)(userdata));
|
||||
coTask._header.append((const char *)buffer, strLen);
|
||||
return strLen;
|
||||
}
|
||||
|
||||
static size_t _outputDataCallbackProc(void *buffer, size_t size, size_t count, void *userdata) {
|
||||
// DLLOG(" _outputDataCallbackProc: size(%ld), count(%ld)", size, count);
|
||||
DownloadTaskCURL *coTask = (DownloadTaskCURL *)userdata;
|
||||
|
||||
// If your callback function returns CURL_WRITEFUNC_PAUSE it will cause this transfer to become paused.
|
||||
return coTask->writeDataProc((unsigned char *)buffer, size, count);
|
||||
}
|
||||
|
||||
// this function designed call in work thread
|
||||
// the curl handle destroyed in _threadProc
|
||||
// handle inited for get header
|
||||
void _initCurlHandleProc(CURL *handle, TaskWrapper &wrapper, bool forContent = false) {
|
||||
const DownloadTask &task = *wrapper.first;
|
||||
const DownloadTaskCURL *coTask = wrapper.second;
|
||||
|
||||
// set url
|
||||
ccstd::string url(task.requestURL);
|
||||
curl_easy_setopt(handle, CURLOPT_URL, StringUtil::replaceAll(url, " ", "%20").c_str());
|
||||
|
||||
// set write func
|
||||
if (forContent) {
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, DownloaderCURL::Impl::_outputDataCallbackProc);
|
||||
} else {
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, DownloaderCURL::Impl::_outputHeaderCallbackProc);
|
||||
}
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEDATA, coTask);
|
||||
|
||||
curl_easy_setopt(handle, CURLOPT_NOPROGRESS, true);
|
||||
// curl_easy_setopt(handle, CURLOPT_XFERINFOFUNCTION, DownloaderCURL::Impl::_progressCallbackProc);
|
||||
// curl_easy_setopt(handle, CURLOPT_XFERINFODATA, coTask);
|
||||
|
||||
curl_easy_setopt(handle, CURLOPT_FAILONERROR, true);
|
||||
curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
|
||||
|
||||
if (forContent) {
|
||||
/** if server acceptRanges and local has part of file, we continue to download **/
|
||||
if (coTask->_acceptRanges && coTask->_totalBytesReceived > 0) {
|
||||
curl_easy_setopt(handle, CURLOPT_RESUME_FROM_LARGE, (curl_off_t)coTask->_totalBytesReceived);
|
||||
}
|
||||
} else {
|
||||
// get header options
|
||||
curl_easy_setopt(handle, CURLOPT_HEADER, 1);
|
||||
curl_easy_setopt(handle, CURLOPT_NOBODY, 1);
|
||||
}
|
||||
|
||||
// if (!sProxy.empty())
|
||||
// {
|
||||
// curl_easy_setopt(curl, CURLOPT_PROXY, sProxy.c_str());
|
||||
// }
|
||||
if (hints.timeoutInSeconds) {
|
||||
curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, hints.timeoutInSeconds);
|
||||
}
|
||||
|
||||
static const long LOW_SPEED_LIMIT = 1;
|
||||
static const long LOW_SPEED_TIME = 10;
|
||||
curl_easy_setopt(handle, CURLOPT_LOW_SPEED_LIMIT, LOW_SPEED_LIMIT);
|
||||
curl_easy_setopt(handle, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME);
|
||||
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
static const int MAX_REDIRS = 5;
|
||||
if (MAX_REDIRS) {
|
||||
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_easy_setopt(handle, CURLOPT_MAXREDIRS, MAX_REDIRS);
|
||||
}
|
||||
}
|
||||
|
||||
// get header info, if success set handle to content download state
|
||||
bool _getHeaderInfoProc(CURL *handle, TaskWrapper &wrapper) {
|
||||
DownloadTaskCURL &coTask = *wrapper.second;
|
||||
CURLcode rc = CURLE_OK;
|
||||
do {
|
||||
long httpResponseCode = 0;
|
||||
rc = curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &httpResponseCode);
|
||||
if (CURLE_OK != rc) {
|
||||
break;
|
||||
}
|
||||
if (200 != httpResponseCode) {
|
||||
char buf[256] = {0};
|
||||
sprintf(buf, "When request url(%s) header info, return unexcept http response code(%ld)", wrapper.first->requestURL.c_str(), httpResponseCode);
|
||||
coTask.setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, CURLE_OK, buf);
|
||||
}
|
||||
|
||||
// curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &effectiveUrl);
|
||||
// curl_easy_getinfo(handle, CURLINFO_CONTENT_TYPE, &contentType);
|
||||
double contentLen = 0;
|
||||
rc = curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &contentLen);
|
||||
if (CURLE_OK != rc) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool acceptRanges = (ccstd::string::npos != coTask._header.find("Accept-Ranges")) ? true : false;
|
||||
|
||||
// get current file size
|
||||
uint32_t fileSize = 0;
|
||||
if (acceptRanges && coTask._tempFileName.length()) {
|
||||
fileSize = FileUtils::getInstance()->getFileSize(coTask._tempFileName);
|
||||
}
|
||||
|
||||
// set header info to coTask
|
||||
std::lock_guard<std::mutex> lock(coTask._mutex);
|
||||
coTask._totalBytesExpected = (uint32_t)contentLen;
|
||||
coTask._acceptRanges = acceptRanges;
|
||||
if (acceptRanges && fileSize > 0) {
|
||||
coTask._totalBytesReceived = fileSize;
|
||||
}
|
||||
coTask._headerAchieved = true;
|
||||
} while (0);
|
||||
|
||||
if (CURLE_OK != rc) {
|
||||
coTask.setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, rc, curl_easy_strerror(rc));
|
||||
}
|
||||
return coTask._headerAchieved;
|
||||
}
|
||||
|
||||
void _threadProc() {
|
||||
DLLOG("++++DownloaderCURL::Impl::_threadProc begin %p", this);
|
||||
// the holder prevent DownloaderCURL::Impl class instance be destruct in main thread
|
||||
auto holder = this->shared_from_this();
|
||||
auto thisThreadId = std::this_thread::get_id();
|
||||
uint32_t countOfMaxProcessingTasks = this->hints.countOfMaxProcessingTasks;
|
||||
// init curl content
|
||||
CURLM *curlmHandle = curl_multi_init();
|
||||
ccstd::unordered_map<CURL *, TaskWrapper> coTaskMap;
|
||||
int runningHandles = 0;
|
||||
CURLMcode mcode = CURLM_OK;
|
||||
int rc = 0; // select return code
|
||||
|
||||
do {
|
||||
// check the thread should exit or not
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_threadMutex);
|
||||
// if the Impl stoped, this->_thread.reset will be called, thus _thread.get_id() not equal with thisThreadId
|
||||
if (thisThreadId != this->_thread.get_id()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (runningHandles) {
|
||||
// get timeout setting from multi-handle
|
||||
long timeoutMS = -1;
|
||||
curl_multi_timeout(curlmHandle, &timeoutMS);
|
||||
|
||||
if (timeoutMS < 0) {
|
||||
timeoutMS = 1000;
|
||||
}
|
||||
|
||||
/* get file descriptors from the transfers */
|
||||
fd_set fdread;
|
||||
fd_set fdwrite;
|
||||
fd_set fdexcep;
|
||||
int maxfd = -1;
|
||||
|
||||
FD_ZERO(&fdread);
|
||||
FD_ZERO(&fdwrite);
|
||||
FD_ZERO(&fdexcep);
|
||||
|
||||
mcode = curl_multi_fdset(curlmHandle, &fdread, &fdwrite, &fdexcep, &maxfd);
|
||||
if (CURLM_OK != mcode) {
|
||||
break;
|
||||
}
|
||||
|
||||
// do wait action
|
||||
if (maxfd == -1) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(CC_CURL_POLL_TIMEOUT_MS));
|
||||
rc = 0;
|
||||
} else {
|
||||
struct timeval timeout;
|
||||
|
||||
timeout.tv_sec = timeoutMS / 1000;
|
||||
timeout.tv_usec = (timeoutMS % 1000) * 1000;
|
||||
|
||||
rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
|
||||
}
|
||||
|
||||
if (rc < 0) {
|
||||
DLLOG(" _threadProc: select return unexpect code: %d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
if (coTaskMap.size()) {
|
||||
mcode = CURLM_CALL_MULTI_PERFORM;
|
||||
while (CURLM_CALL_MULTI_PERFORM == mcode) {
|
||||
mcode = curl_multi_perform(curlmHandle, &runningHandles);
|
||||
}
|
||||
if (CURLM_OK != mcode) {
|
||||
break;
|
||||
}
|
||||
|
||||
struct CURLMsg *m;
|
||||
do {
|
||||
int msgq = 0;
|
||||
m = curl_multi_info_read(curlmHandle, &msgq);
|
||||
if (m && (m->msg == CURLMSG_DONE)) {
|
||||
CURL *curlHandle = m->easy_handle;
|
||||
CURLcode errCode = m->data.result;
|
||||
|
||||
TaskWrapper wrapper = coTaskMap[curlHandle];
|
||||
|
||||
// remove from multi-handle
|
||||
curl_multi_remove_handle(curlmHandle, curlHandle);
|
||||
bool reinited = false;
|
||||
do {
|
||||
if (CURLE_OK != errCode) {
|
||||
wrapper.second->setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, errCode, curl_easy_strerror(errCode));
|
||||
break;
|
||||
}
|
||||
|
||||
// if the task is content download task, cleanup the handle
|
||||
if (wrapper.second->_headerAchieved) {
|
||||
break;
|
||||
}
|
||||
|
||||
// the task is get header task
|
||||
// first, we get info from response
|
||||
if (false == _getHeaderInfoProc(curlHandle, wrapper)) {
|
||||
// the error info has been set in _getHeaderInfoProc
|
||||
break;
|
||||
}
|
||||
|
||||
// after get header info success
|
||||
// wrapper.second->_totalBytesReceived inited by local file size
|
||||
// if the local file size equal with the content size from header, the file has downloaded finish
|
||||
if (wrapper.second->_totalBytesReceived &&
|
||||
wrapper.second->_totalBytesReceived == wrapper.second->_totalBytesExpected) {
|
||||
// the file has download complete
|
||||
// break to move this task to finish queue
|
||||
break;
|
||||
}
|
||||
// reinit curl handle for download content
|
||||
curl_easy_reset(curlHandle);
|
||||
_initCurlHandleProc(curlHandle, wrapper, true);
|
||||
mcode = curl_multi_add_handle(curlmHandle, curlHandle);
|
||||
if (CURLM_OK != mcode) {
|
||||
wrapper.second->setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, mcode, curl_multi_strerror(mcode));
|
||||
break;
|
||||
}
|
||||
reinited = true;
|
||||
} while (0);
|
||||
|
||||
if (reinited) {
|
||||
continue;
|
||||
}
|
||||
curl_easy_cleanup(curlHandle);
|
||||
DLLOG(" _threadProc task clean cur handle :%p with errCode:%d", curlHandle, errCode);
|
||||
|
||||
// remove from coTaskMap
|
||||
coTaskMap.erase(curlHandle);
|
||||
|
||||
// remove from _processSet
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_processMutex);
|
||||
if (_processSet.end() != _processSet.find(wrapper)) {
|
||||
_processSet.erase(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
// add to finishedQueue
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_finishedMutex);
|
||||
_finishedQueue.push_back(wrapper);
|
||||
}
|
||||
}
|
||||
} while (m);
|
||||
}
|
||||
|
||||
// process tasks in _requestList
|
||||
auto size = coTaskMap.size();
|
||||
while (0 == countOfMaxProcessingTasks || size < countOfMaxProcessingTasks) {
|
||||
// get task wrapper from request queue
|
||||
TaskWrapper wrapper;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_requestMutex);
|
||||
if (_requestQueue.size()) {
|
||||
wrapper = _requestQueue.front();
|
||||
_requestQueue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
// if request queue is empty, the wrapper.first is nullptr
|
||||
if (!wrapper.first) {
|
||||
break;
|
||||
}
|
||||
|
||||
wrapper.second->initProc();
|
||||
|
||||
// create curl handle from task and add into curl multi handle
|
||||
CURL *curlHandle = curl_easy_init();
|
||||
|
||||
if (nullptr == curlHandle) {
|
||||
wrapper.second->setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, 0, "Alloc curl handle failed.");
|
||||
std::lock_guard<std::mutex> lock(_finishedMutex);
|
||||
_finishedQueue.push_back(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
// init curl handle for get header info
|
||||
_initCurlHandleProc(curlHandle, wrapper);
|
||||
|
||||
// add curl handle to process list
|
||||
mcode = curl_multi_add_handle(curlmHandle, curlHandle);
|
||||
if (CURLM_OK != mcode) {
|
||||
wrapper.second->setErrorProc(DownloadTask::ERROR_IMPL_INTERNAL, mcode, curl_multi_strerror(mcode));
|
||||
std::lock_guard<std::mutex> lock(_finishedMutex);
|
||||
_finishedQueue.push_back(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
DLLOG(" _threadProc task create curl handle:%p", curlHandle);
|
||||
coTaskMap[curlHandle] = wrapper;
|
||||
std::lock_guard<std::mutex> lock(_processMutex);
|
||||
_processSet.insert(wrapper);
|
||||
}
|
||||
} while (coTaskMap.size());
|
||||
|
||||
curl_multi_cleanup(curlmHandle);
|
||||
this->stop();
|
||||
DLLOG("----DownloaderCURL::Impl::_threadProc end");
|
||||
}
|
||||
|
||||
std::thread _thread;
|
||||
ccstd::deque<TaskWrapper> _requestQueue;
|
||||
ccstd::set<TaskWrapper> _processSet;
|
||||
ccstd::deque<TaskWrapper> _finishedQueue;
|
||||
|
||||
std::mutex _threadMutex;
|
||||
std::mutex _requestMutex;
|
||||
std::mutex _processMutex;
|
||||
std::mutex _finishedMutex;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation DownloaderCURL
|
||||
DownloaderCURL::DownloaderCURL(const DownloaderHints &hints)
|
||||
: _impl(std::make_shared<Impl>()),
|
||||
_currTask(nullptr) {
|
||||
DLLOG("Construct DownloaderCURL %p", this);
|
||||
_impl->hints = hints;
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
|
||||
_transferDataToBuffer = [this](void *buf, uint32_t len) -> uint32_t {
|
||||
DownloadTaskCURL &coTask = *_currTask;
|
||||
uint32_t dataLen = coTask._buf.size();
|
||||
if (len < dataLen) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(buf, coTask._buf.data(), dataLen);
|
||||
coTask._buf.resize(0);
|
||||
return dataLen;
|
||||
};
|
||||
|
||||
char key[128];
|
||||
sprintf(key, "DownloaderCURL(%p)", this);
|
||||
_schedulerKey = key;
|
||||
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->schedule(std::bind(&DownloaderCURL::onSchedule, this, std::placeholders::_1),
|
||||
this,
|
||||
0.1f,
|
||||
true,
|
||||
_schedulerKey);
|
||||
}
|
||||
}
|
||||
|
||||
DownloaderCURL::~DownloaderCURL() {
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->unschedule(_schedulerKey, this);
|
||||
}
|
||||
|
||||
_impl->stop();
|
||||
DLLOG("Destruct DownloaderCURL %p", this);
|
||||
}
|
||||
|
||||
IDownloadTask *DownloaderCURL::createCoTask(std::shared_ptr<const DownloadTask> &task) {
|
||||
DownloadTaskCURL *coTask = ccnew DownloadTaskCURL;
|
||||
coTask->init(task->storagePath, _impl->hints.tempFileNameSuffix);
|
||||
|
||||
DLLOG(" DownloaderCURL: createTask: Id(%d)", coTask->serialId);
|
||||
|
||||
_impl->addTask(task, coTask);
|
||||
_impl->run();
|
||||
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->resumeTarget(this);
|
||||
}
|
||||
return coTask;
|
||||
}
|
||||
|
||||
void DownloaderCURL::abort(const std::unique_ptr<IDownloadTask> &task) {
|
||||
// REFINE
|
||||
// https://github.com/cocos-creator/cocos2d-x-lite/pull/1291
|
||||
DLLOG("%s isn't implemented!\n", __FUNCTION__);
|
||||
}
|
||||
|
||||
void DownloaderCURL::onSchedule(float) {
|
||||
ccstd::vector<TaskWrapper> tasks;
|
||||
|
||||
// update processing tasks
|
||||
_impl->getProcessTasks(tasks);
|
||||
for (auto &wrapper : tasks) {
|
||||
const DownloadTask &task = *wrapper.first;
|
||||
DownloadTaskCURL &coTask = *wrapper.second;
|
||||
|
||||
std::lock_guard<std::mutex> lock(coTask._mutex);
|
||||
if (coTask._bytesReceived) {
|
||||
_currTask = &coTask;
|
||||
onTaskProgress(task,
|
||||
coTask._bytesReceived,
|
||||
coTask._totalBytesReceived,
|
||||
coTask._totalBytesExpected,
|
||||
_transferDataToBuffer);
|
||||
_currTask = nullptr;
|
||||
coTask._bytesReceived = 0;
|
||||
}
|
||||
}
|
||||
tasks.resize(0);
|
||||
|
||||
// update finished tasks
|
||||
_impl->getFinishedTasks(tasks);
|
||||
if (_impl->stoped()) {
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->pauseTarget(this);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &wrapper : tasks) {
|
||||
const DownloadTask &task = *wrapper.first;
|
||||
DownloadTaskCURL &coTask = *wrapper.second;
|
||||
|
||||
// if there is bytesReceived, call progress update first
|
||||
if (coTask._bytesReceived) {
|
||||
_currTask = &coTask;
|
||||
onTaskProgress(task,
|
||||
coTask._bytesReceived,
|
||||
coTask._totalBytesReceived,
|
||||
coTask._totalBytesExpected,
|
||||
_transferDataToBuffer);
|
||||
coTask._bytesReceived = 0;
|
||||
_currTask = nullptr;
|
||||
}
|
||||
|
||||
// if file task, close file handle and rename file if needed
|
||||
if (coTask._fp) {
|
||||
fclose(coTask._fp);
|
||||
coTask._fp = nullptr;
|
||||
do {
|
||||
if (0 == coTask._fileName.length()) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto util = FileUtils::getInstance();
|
||||
// if file already exist, remove it
|
||||
if (util->isFileExist(coTask._fileName)) {
|
||||
if (false == util->removeFile(coTask._fileName)) {
|
||||
coTask._errCode = DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
coTask._errCodeInternal = 0;
|
||||
coTask._errDescription = "Can't remove old file: ";
|
||||
coTask._errDescription.append(coTask._fileName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// rename file
|
||||
if (util->renameFile(coTask._tempFileName, coTask._fileName)) {
|
||||
// success, remove storage from set
|
||||
DownloadTaskCURL::_sStoragePathSet.erase(coTask._tempFileName);
|
||||
break;
|
||||
}
|
||||
// failed
|
||||
coTask._errCode = DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
coTask._errCodeInternal = 0;
|
||||
coTask._errDescription = "Can't renamefile from: ";
|
||||
coTask._errDescription.append(coTask._tempFileName);
|
||||
coTask._errDescription.append(" to: ");
|
||||
coTask._errDescription.append(coTask._fileName);
|
||||
} while (0);
|
||||
}
|
||||
// needn't lock coTask here, because tasks has removed form _impl
|
||||
onTaskFinish(task, coTask._errCode, coTask._errCodeInternal, coTask._errDescription, coTask._buf);
|
||||
DLLOG(" DownloaderCURL: finish Task: Id(%d)", coTask.serialId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
63
cocos/network/Downloader-curl.h
Normal file
63
cocos/network/Downloader-curl.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "network/DownloaderImpl.h"
|
||||
|
||||
namespace cc {
|
||||
class Scheduler;
|
||||
}
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
class DownloadTaskCURL;
|
||||
struct DownloaderHints;
|
||||
|
||||
class DownloaderCURL : public IDownloaderImpl {
|
||||
public:
|
||||
explicit DownloaderCURL(const DownloaderHints &hints);
|
||||
~DownloaderCURL() override;
|
||||
|
||||
IDownloadTask *createCoTask(std::shared_ptr<const DownloadTask> &task) override;
|
||||
|
||||
void abort(const std::unique_ptr<IDownloadTask> &task) override;
|
||||
|
||||
protected:
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> _impl;
|
||||
|
||||
// for transfer data on schedule
|
||||
DownloadTaskCURL *_currTask; // temp ref
|
||||
std::function<uint32_t(void *, uint32_t)> _transferDataToBuffer;
|
||||
|
||||
// scheduler for update processing and finished task in main schedule
|
||||
void onSchedule(float);
|
||||
ccstd::string _schedulerKey;
|
||||
std::weak_ptr<Scheduler> _scheduler;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
293
cocos/network/Downloader-java.cpp
Normal file
293
cocos/network/Downloader-java.cpp
Normal file
@@ -0,0 +1,293 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/Downloader-java.h"
|
||||
|
||||
#include <mutex>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/StringUtil.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "network/Downloader.h"
|
||||
#include "platform/java/jni/JniHelper.h"
|
||||
#include "platform/java/jni/JniImp.h"
|
||||
|
||||
#ifndef JCLS_DOWNLOADER
|
||||
#define JCLS_DOWNLOADER "com/cocos/lib/CocosDownloader"
|
||||
#endif
|
||||
#define JARG_STR "Ljava/lang/String;"
|
||||
#define JARG_DOWNLOADER "L" JCLS_DOWNLOADER ";"
|
||||
|
||||
#ifndef ORG_DOWNLOADER_CLASS_NAME
|
||||
#define ORG_DOWNLOADER_CLASS_NAME com_cocos_lib_CocosDownloader
|
||||
#endif
|
||||
#define JNI_DOWNLOADER(FUNC) JNI_METHOD1(ORG_DOWNLOADER_CLASS_NAME, FUNC)
|
||||
|
||||
ccstd::unordered_map<int, cc::network::DownloaderJava *> sDownloaderMap;
|
||||
std::mutex sDownloaderMutex;
|
||||
|
||||
static void insertDownloaderJava(int id, cc::network::DownloaderJava *downloaderPtr) {
|
||||
std::lock_guard<std::mutex> guard(sDownloaderMutex);
|
||||
sDownloaderMap.insert(std::make_pair(id, downloaderPtr));
|
||||
}
|
||||
|
||||
static void eraseDownloaderJava(int id) {
|
||||
std::lock_guard<std::mutex> guard(sDownloaderMutex);
|
||||
sDownloaderMap.erase(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* If not found, return nullptr, otherwise return the Downloader
|
||||
*/
|
||||
static cc::network::DownloaderJava *findDownloaderJava(int id) {
|
||||
std::lock_guard<std::mutex> guard(sDownloaderMutex);
|
||||
auto iter = sDownloaderMap.find(id);
|
||||
if (sDownloaderMap.end() == iter) {
|
||||
return nullptr;
|
||||
}
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
static int sTaskCounter = 0;
|
||||
static int sDownloaderCounter = 0;
|
||||
|
||||
struct DownloadTaskAndroid : public IDownloadTask {
|
||||
DownloadTaskAndroid()
|
||||
: id(++sTaskCounter) {
|
||||
DLLOG("Construct DownloadTaskAndroid: %p", this);
|
||||
}
|
||||
~DownloadTaskAndroid() override {
|
||||
DLLOG("Destruct DownloadTaskAndroid: %p", this);
|
||||
}
|
||||
|
||||
int id;
|
||||
std::shared_ptr<const DownloadTask> task; // reference to DownloadTask, when task finish, release
|
||||
};
|
||||
|
||||
DownloaderJava::DownloaderJava(const DownloaderHints &hints)
|
||||
: _id(++sDownloaderCounter),
|
||||
_impl(nullptr) {
|
||||
DLLOG("Construct DownloaderJava: %p", this);
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_DOWNLOADER,
|
||||
"createDownloader",
|
||||
"(II" JARG_STR "I)" JARG_DOWNLOADER)) {
|
||||
jobject jStr = methodInfo.env->NewStringUTF(hints.tempFileNameSuffix.c_str());
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID,
|
||||
methodInfo.methodID,
|
||||
_id,
|
||||
hints.timeoutInSeconds,
|
||||
jStr,
|
||||
hints.countOfMaxProcessingTasks);
|
||||
_impl = methodInfo.env->NewGlobalRef(jObj);
|
||||
DLLOG("android downloader: jObj: %p, _impl: %p", jObj, _impl);
|
||||
//It's not thread-safe here, use thread-safe method instead
|
||||
//sDownloaderMap.insert(make_pair(_id, this));
|
||||
insertDownloaderJava(_id, this);
|
||||
ccDeleteLocalRef(methodInfo.env, jStr);
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
}
|
||||
}
|
||||
|
||||
DownloaderJava::~DownloaderJava() {
|
||||
if (_impl != nullptr) {
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_DOWNLOADER,
|
||||
"cancelAllRequests",
|
||||
"(" JARG_DOWNLOADER ")V")) {
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID,
|
||||
methodInfo.methodID,
|
||||
_impl);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
}
|
||||
//It's not thread-safe here, use thread-safe method instead
|
||||
//sDownloaderMap.erase(_id);
|
||||
eraseDownloaderJava(_id);
|
||||
JniHelper::getEnv()->DeleteGlobalRef(_impl);
|
||||
}
|
||||
DLLOG("Destruct DownloaderJava: %p", this);
|
||||
}
|
||||
|
||||
IDownloadTask *DownloaderJava::createCoTask(std::shared_ptr<const DownloadTask> &task) {
|
||||
auto *coTask = ccnew DownloadTaskAndroid;
|
||||
coTask->task = task;
|
||||
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_DOWNLOADER,
|
||||
"createTask",
|
||||
"(" JARG_DOWNLOADER "I" JARG_STR JARG_STR "[" JARG_STR ")V")) {
|
||||
jclass jclassString = methodInfo.env->FindClass("java/lang/String");
|
||||
ccstd::string url(task->requestURL);
|
||||
jstring jstrURL = methodInfo.env->NewStringUTF(StringUtil::replaceAll(url, " ", "%20").c_str());
|
||||
jstring jstrPath = methodInfo.env->NewStringUTF(task->storagePath.c_str());
|
||||
jobjectArray jarrayHeader = methodInfo.env->NewObjectArray(task->header.size() * 2, jclassString, nullptr);
|
||||
const ccstd::unordered_map<ccstd::string, ccstd::string> &headMap = task->header;
|
||||
int index = 0;
|
||||
for (const auto &it : headMap) {
|
||||
methodInfo.env->SetObjectArrayElement(jarrayHeader, index++, methodInfo.env->NewStringUTF(it.first.c_str()));
|
||||
methodInfo.env->SetObjectArrayElement(jarrayHeader, index++, methodInfo.env->NewStringUTF(it.second.c_str()));
|
||||
}
|
||||
methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, _impl, coTask->id, jstrURL, jstrPath, jarrayHeader);
|
||||
for (int i = 0; i < index; ++i) {
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.env->GetObjectArrayElement(jarrayHeader, i));
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, jclassString);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrURL);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrPath);
|
||||
ccDeleteLocalRef(methodInfo.env, jarrayHeader);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
}
|
||||
|
||||
DLLOG("DownloaderJava::createCoTask id: %d", coTask->id);
|
||||
_taskMap.insert(std::make_pair(coTask->id, coTask));
|
||||
return coTask;
|
||||
}
|
||||
|
||||
void DownloaderJava::abort(const std::unique_ptr<IDownloadTask> &task) {
|
||||
auto iter = _taskMap.begin();
|
||||
for (; iter != _taskMap.end(); iter++) {
|
||||
if (task.get() == iter->second) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_impl != nullptr && iter != _taskMap.end()) {
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_DOWNLOADER,
|
||||
"abort",
|
||||
"(" JARG_DOWNLOADER "I"
|
||||
")V")) {
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID,
|
||||
methodInfo.methodID,
|
||||
_impl,
|
||||
iter->first);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
|
||||
DownloadTaskAndroid *coTask = iter->second;
|
||||
_taskMap.erase(iter);
|
||||
ccstd::vector<unsigned char> emptyBuffer;
|
||||
onTaskFinish(*coTask->task,
|
||||
DownloadTask::ERROR_ABORT,
|
||||
DownloadTask::ERROR_ABORT,
|
||||
"downloadFile:fail abort",
|
||||
emptyBuffer);
|
||||
coTask->task.reset();
|
||||
}
|
||||
}
|
||||
DLLOG("DownloaderJava:abort");
|
||||
}
|
||||
|
||||
void DownloaderJava::onProcessImpl(int taskId, uint32_t dl, uint32_t dlNow, uint32_t dlTotal) {
|
||||
DLLOG("DownloaderJava::onProgress(taskId: %d, dl: %lld, dlnow: %lld, dltotal: %lld)", taskId, dl, dlNow, dlTotal);
|
||||
auto iter = _taskMap.find(taskId);
|
||||
if (_taskMap.end() == iter) {
|
||||
DLLOG("DownloaderJava::onProgress can't find task with id: %d", taskId);
|
||||
return;
|
||||
}
|
||||
DownloadTaskAndroid *coTask = iter->second;
|
||||
std::function<uint32_t(void *, uint32_t)> transferDataToBuffer;
|
||||
onTaskProgress(*coTask->task, dl, dlNow, dlTotal, transferDataToBuffer);
|
||||
}
|
||||
|
||||
void DownloaderJava::onFinishImpl(int taskId, int errCode, const char *errStr, const ccstd::vector<unsigned char> &data) {
|
||||
DLLOG("DownloaderJava::onFinishImpl(taskId: %d, errCode: %d, errStr: %s)", taskId, errCode, (errStr) ? errStr : "null");
|
||||
auto iter = _taskMap.find(taskId);
|
||||
if (_taskMap.end() == iter) {
|
||||
DLLOG("DownloaderJava::onFinishImpl can't find task with id: %d", taskId);
|
||||
return;
|
||||
}
|
||||
DownloadTaskAndroid *coTask = iter->second;
|
||||
ccstd::string str = (errStr ? errStr : "");
|
||||
_taskMap.erase(iter);
|
||||
onTaskFinish(*coTask->task,
|
||||
(errStr || (errCode != 0)) ? DownloadTask::ERROR_IMPL_INTERNAL : DownloadTask::ERROR_NO_ERROR,
|
||||
errCode,
|
||||
str,
|
||||
data);
|
||||
coTask->task.reset();
|
||||
}
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL JNI_DOWNLOADER(nativeOnProgress)(JNIEnv * /*env*/, jobject /*obj*/, jint id, jint taskId, jlong dl, jlong dlnow, jlong dltotal) {
|
||||
auto func = [=]() -> void {
|
||||
DLLOG("_nativeOnProgress(id: %d, taskId: %d, dl: %lld, dlnow: %lld, dltotal: %lld)", id, taskId, dl, dlnow, dltotal);
|
||||
//It's not thread-safe here, use thread-safe method instead
|
||||
cc::network::DownloaderJava *downloader = findDownloaderJava(id);
|
||||
if (nullptr == downloader) {
|
||||
DLLOG("_nativeOnProgress can't find downloader by key: %p for task: %d", clazz, id);
|
||||
return;
|
||||
}
|
||||
downloader->onProcessImpl((int)taskId, (uint32_t)dl, (uint32_t)dlnow, (uint32_t)dltotal);
|
||||
};
|
||||
CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread(func);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL JNI_DOWNLOADER(nativeOnFinish)(JNIEnv *env, jobject /*obj*/, jint id, jint taskId, jint errCode, jstring errStr, jbyteArray data) {
|
||||
ccstd::string errStrTmp;
|
||||
ccstd::vector<uint8_t> dataTmp;
|
||||
if (errStr) {
|
||||
const char *nativeErrStr = env->GetStringUTFChars(errStr, JNI_FALSE);
|
||||
errStrTmp = nativeErrStr;
|
||||
env->ReleaseStringUTFChars(errStr, nativeErrStr);
|
||||
}
|
||||
if (data && env->GetArrayLength(data) > 0) {
|
||||
auto len = env->GetArrayLength(data);
|
||||
dataTmp.resize(len);
|
||||
env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte *>(dataTmp.data()));
|
||||
}
|
||||
auto func = [errStrTmp = std::move(errStrTmp), dataTmp = std::move(dataTmp), id, taskId, errCode]() -> void {
|
||||
DLLOG("_nativeOnFinish(id: %d, taskId: %d)", id, taskId);
|
||||
//It's not thread-safe here, use thread-safe method instead
|
||||
cc::network::DownloaderJava *downloader = findDownloaderJava(id);
|
||||
if (nullptr == downloader) {
|
||||
DLLOG("_nativeOnFinish can't find downloader id: %d for task: %d", id, taskId);
|
||||
return;
|
||||
}
|
||||
ccstd::vector<unsigned char> buf;
|
||||
if (!errStrTmp.empty()) {
|
||||
// failure
|
||||
downloader->onFinishImpl((int)taskId, (int)errCode, errStrTmp.c_str(), buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// success
|
||||
downloader->onFinishImpl((int)taskId, (int)errCode, nullptr, dataTmp);
|
||||
};
|
||||
CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread(func);
|
||||
}
|
||||
|
||||
} // extern "C" {
|
||||
56
cocos/network/Downloader-java.h
Normal file
56
cocos/network/Downloader-java.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "network/DownloaderImpl.h"
|
||||
|
||||
class _jobject; // NOLINT(bugprone-reserved-identifier)
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
class DownloadTaskAndroid;
|
||||
struct DownloaderHints;
|
||||
|
||||
class DownloaderJava : public IDownloaderImpl {
|
||||
public:
|
||||
explicit DownloaderJava(const DownloaderHints &hints);
|
||||
~DownloaderJava() override;
|
||||
|
||||
IDownloadTask *createCoTask(std::shared_ptr<const DownloadTask> &task) override;
|
||||
|
||||
void abort(const std::unique_ptr<IDownloadTask> &task) override;
|
||||
|
||||
// designed called by internal
|
||||
void onProcessImpl(int taskId, uint32_t dl, uint32_t dlNow, uint32_t dlTotal);
|
||||
void onFinishImpl(int taskId, int errCode, const char *errStr, const ccstd::vector<unsigned char> &data);
|
||||
|
||||
protected:
|
||||
int _id;
|
||||
_jobject *_impl;
|
||||
ccstd::unordered_map<int, DownloadTaskAndroid *> _taskMap;
|
||||
};
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
173
cocos/network/Downloader.cpp
Normal file
173
cocos/network/Downloader.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/Downloader.h"
|
||||
#include <memory>
|
||||
#include "base/memory/Memory.h"
|
||||
|
||||
// include platform specific implement class
|
||||
#if (CC_PLATFORM == CC_PLATFORM_MACOS || CC_PLATFORM == CC_PLATFORM_IOS)
|
||||
|
||||
#include "network/DownloaderImpl-apple.h"
|
||||
#define DownloaderImpl DownloaderApple //NOLINT(readability-identifier-naming)
|
||||
|
||||
#elif (CC_PLATFORM == CC_PLATFORM_ANDROID || CC_PLATFORM == CC_PLATFORM_OHOS)
|
||||
|
||||
#include "network/Downloader-java.h"
|
||||
#define DownloaderImpl DownloaderJava //NOLINT(readability-identifier-naming)
|
||||
|
||||
#else
|
||||
|
||||
#include "network/Downloader-curl.h"
|
||||
#define DownloaderImpl DownloaderCURL //NOLINT(readability-identifier-naming)
|
||||
|
||||
#endif
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
DownloadTask::DownloadTask() {
|
||||
DLLOG("Construct DownloadTask %p", this);
|
||||
}
|
||||
|
||||
DownloadTask::~DownloadTask() {
|
||||
DLLOG("Destruct DownloadTask %p", this);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement Downloader
|
||||
Downloader::Downloader() {
|
||||
DownloaderHints hints;
|
||||
ccnew_placement(this) Downloader(hints);
|
||||
}
|
||||
|
||||
Downloader::Downloader(const DownloaderHints &hints) {
|
||||
DLLOG("Construct Downloader %p", this);
|
||||
_impl = std::make_unique<DownloaderImpl>(hints);
|
||||
_impl->onTaskProgress = [this](const DownloadTask &task,
|
||||
uint32_t bytesReceived,
|
||||
uint32_t totalBytesReceived,
|
||||
uint32_t totalBytesExpected,
|
||||
std::function<uint32_t(void *buffer, uint32_t len)> & /*transferDataToBuffer*/) {
|
||||
if (onTaskProgress) {
|
||||
onTaskProgress(task, bytesReceived, totalBytesReceived, totalBytesExpected);
|
||||
}
|
||||
};
|
||||
|
||||
_impl->onTaskFinish = [this](const DownloadTask &task,
|
||||
int errorCode,
|
||||
int errorCodeInternal,
|
||||
const ccstd::string &errorStr,
|
||||
const ccstd::vector<unsigned char> &data) {
|
||||
if (DownloadTask::ERROR_NO_ERROR != errorCode) {
|
||||
if (onTaskError) {
|
||||
onTaskError(task, errorCode, errorCodeInternal, errorStr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// success callback
|
||||
if (task.storagePath.length()) {
|
||||
if (onFileTaskSuccess) {
|
||||
onFileTaskSuccess(task);
|
||||
}
|
||||
} else {
|
||||
// data task
|
||||
if (onDataTaskSuccess) {
|
||||
onDataTaskSuccess(task, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Downloader::~Downloader() {
|
||||
DLLOG("Destruct Downloader %p", this);
|
||||
}
|
||||
|
||||
std::shared_ptr<const DownloadTask> Downloader::createDataTask(const ccstd::string &srcUrl, const ccstd::string &identifier /* = ""*/) {
|
||||
auto *iTask = ccnew DownloadTask();
|
||||
std::shared_ptr<const DownloadTask> task(iTask);
|
||||
do {
|
||||
iTask->requestURL = srcUrl;
|
||||
iTask->identifier = identifier;
|
||||
if (0 == srcUrl.length()) {
|
||||
if (onTaskError) {
|
||||
onTaskError(*task, DownloadTask::ERROR_INVALID_PARAMS, 0, "URL or is empty.");
|
||||
}
|
||||
task.reset();
|
||||
break;
|
||||
}
|
||||
iTask->_coTask.reset(_impl->createCoTask(task));
|
||||
} while (false);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
std::shared_ptr<const DownloadTask> Downloader::createDownloadTask(const ccstd::string &srcUrl,
|
||||
const ccstd::string &storagePath,
|
||||
const ccstd::unordered_map<ccstd::string, ccstd::string> &header,
|
||||
const ccstd::string &identifier /* = ""*/) {
|
||||
auto *iTask = ccnew DownloadTask();
|
||||
std::shared_ptr<const DownloadTask> task(iTask);
|
||||
do {
|
||||
iTask->requestURL = srcUrl;
|
||||
iTask->storagePath = storagePath;
|
||||
iTask->identifier = identifier;
|
||||
iTask->header = header;
|
||||
if (0 == srcUrl.length() || 0 == storagePath.length()) {
|
||||
if (onTaskError) {
|
||||
onTaskError(*task, DownloadTask::ERROR_INVALID_PARAMS, 0, "URL or storage path is empty.");
|
||||
}
|
||||
task.reset();
|
||||
break;
|
||||
}
|
||||
iTask->_coTask.reset(_impl->createCoTask(task));
|
||||
} while (false);
|
||||
|
||||
return task;
|
||||
}
|
||||
std::shared_ptr<const DownloadTask> Downloader::createDownloadTask(const ccstd::string &srcUrl,
|
||||
const ccstd::string &storagePath,
|
||||
const ccstd::string &identifier /* = ""*/) {
|
||||
const ccstd::unordered_map<ccstd::string, ccstd::string> emptyHeader;
|
||||
return createDownloadTask(srcUrl, storagePath, emptyHeader, identifier);
|
||||
}
|
||||
|
||||
void Downloader::abort(const std::shared_ptr<const DownloadTask> &task) {
|
||||
_impl->abort(task->_coTask);
|
||||
}
|
||||
//ccstd::string Downloader::getFileNameFromUrl(const ccstd::string& srcUrl)
|
||||
//{
|
||||
// // Find file name and file extension
|
||||
// ccstd::string filename;
|
||||
// unsigned long found = srcUrl.find_last_of("/\\");
|
||||
// if (found != ccstd::string::npos)
|
||||
// filename = srcUrl.substr(found+1);
|
||||
// return filename;
|
||||
//}
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
125
cocos/network/Downloader.h
Normal file
125
cocos/network/Downloader.h
Normal file
@@ -0,0 +1,125 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include "base/Macros.h"
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/unordered_map.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
class IDownloadTask;
|
||||
class IDownloaderImpl;
|
||||
class Downloader;
|
||||
|
||||
class CC_DLL DownloadTask final {
|
||||
public:
|
||||
static const int ERROR_NO_ERROR = 0;
|
||||
static const int ERROR_INVALID_PARAMS = -1;
|
||||
static const int ERROR_FILE_OP_FAILED = -2;
|
||||
static const int ERROR_IMPL_INTERNAL = -3;
|
||||
static const int ERROR_ABORT = -4;
|
||||
|
||||
ccstd::string identifier;
|
||||
ccstd::string requestURL;
|
||||
ccstd::string storagePath;
|
||||
ccstd::unordered_map<ccstd::string, ccstd::string> header;
|
||||
|
||||
DownloadTask();
|
||||
virtual ~DownloadTask();
|
||||
|
||||
private:
|
||||
friend class Downloader;
|
||||
std::unique_ptr<IDownloadTask> _coTask;
|
||||
};
|
||||
|
||||
struct CC_DLL DownloaderHints {
|
||||
uint32_t countOfMaxProcessingTasks{6};
|
||||
uint32_t timeoutInSeconds{45};
|
||||
ccstd::string tempFileNameSuffix{".tmp"};
|
||||
};
|
||||
|
||||
class CC_DLL Downloader final {
|
||||
public:
|
||||
Downloader();
|
||||
explicit Downloader(const DownloaderHints &hints);
|
||||
~Downloader();
|
||||
|
||||
std::function<void(const DownloadTask &task,
|
||||
const ccstd::vector<unsigned char> &data)>
|
||||
onDataTaskSuccess;
|
||||
|
||||
std::function<void(const DownloadTask &task)> onFileTaskSuccess;
|
||||
|
||||
std::function<void(const DownloadTask &task,
|
||||
uint32_t bytesReceived,
|
||||
uint32_t totalBytesReceived,
|
||||
uint32_t totalBytesExpected)>
|
||||
onTaskProgress;
|
||||
|
||||
std::function<void(const DownloadTask &task,
|
||||
int errorCode,
|
||||
int errorCodeInternal,
|
||||
const ccstd::string &errorStr)>
|
||||
onTaskError;
|
||||
|
||||
void setOnSuccess(const std::function<void(const DownloadTask &task)> &callback) { onFileTaskSuccess = callback; };
|
||||
|
||||
void setOnProgress(const std::function<void(const DownloadTask &task,
|
||||
uint32_t bytesReceived,
|
||||
uint32_t totalBytesReceived,
|
||||
uint32_t totalBytesExpected)> &callback) { onTaskProgress = callback; };
|
||||
|
||||
void setOnError(const std::function<void(const DownloadTask &task,
|
||||
int errorCode,
|
||||
int errorCodeInternal,
|
||||
const ccstd::string &errorStr)> &callback) { onTaskError = callback; };
|
||||
|
||||
// CC_DEPRECATED(3.6, "Use setOnProgress instead") // needed for bindings, so not uncomment this line
|
||||
void setOnTaskProgress(const std::function<void(const DownloadTask &task,
|
||||
uint32_t bytesReceived,
|
||||
uint32_t totalBytesReceived,
|
||||
uint32_t totalBytesExpected)> &callback) { onTaskProgress = callback; };
|
||||
|
||||
std::shared_ptr<const DownloadTask> createDataTask(const ccstd::string &srcUrl, const ccstd::string &identifier = "");
|
||||
|
||||
std::shared_ptr<const DownloadTask> createDownloadTask(const ccstd::string &srcUrl, const ccstd::string &storagePath, const ccstd::string &identifier = "");
|
||||
|
||||
std::shared_ptr<const DownloadTask> createDownloadTask(const ccstd::string &srcUrl, const ccstd::string &storagePath, const ccstd::unordered_map<ccstd::string, ccstd::string> &header, const ccstd::string &identifier = "");
|
||||
|
||||
void abort(const std::shared_ptr<const DownloadTask> &task);
|
||||
|
||||
private:
|
||||
std::unique_ptr<IDownloaderImpl> _impl;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
46
cocos/network/DownloaderImpl-apple.h
Normal file
46
cocos/network/DownloaderImpl-apple.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "network/DownloaderImpl.h"
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
struct DownloaderHints;
|
||||
class DownloaderApple : public IDownloaderImpl {
|
||||
public:
|
||||
DownloaderApple(const DownloaderHints &hints);
|
||||
virtual ~DownloaderApple();
|
||||
|
||||
virtual IDownloadTask *createCoTask(std::shared_ptr<const DownloadTask> &task) override;
|
||||
|
||||
virtual void abort(const std::unique_ptr<IDownloadTask> &task) override;
|
||||
|
||||
private:
|
||||
void *_impl;
|
||||
};
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
705
cocos/network/DownloaderImpl-apple.mm
Normal file
705
cocos/network/DownloaderImpl-apple.mm
Normal file
@@ -0,0 +1,705 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/DownloaderImpl-apple.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "base/std/container/queue.h"
|
||||
#include "base/UTF8.h"
|
||||
#include "network/Downloader.h"
|
||||
#include "platform/FileUtils.h"
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// OC Classes Declaration
|
||||
|
||||
// this wrapper used to wrap C++ class DownloadTask into NSMutableDictionary
|
||||
@interface DownloadTaskWrapper : NSObject {
|
||||
std::shared_ptr<const cc::network::DownloadTask> _task;
|
||||
NSMutableArray *_dataArray;
|
||||
}
|
||||
// temp vars for dataTask: didReceivedData callback
|
||||
@property (nonatomic) uint32_t bytesReceived;
|
||||
@property (nonatomic) uint32_t totalBytesReceived;
|
||||
|
||||
- (id)init:(std::shared_ptr<const cc::network::DownloadTask> &)t;
|
||||
- (const cc::network::DownloadTask *)get;
|
||||
- (void)addData:(NSData *)data;
|
||||
- (uint32_t)transferDataToBuffer:(void *)buffer lengthOfBuffer:(uint32_t)len;
|
||||
|
||||
@end
|
||||
|
||||
@interface DownloaderAppleImpl : NSObject <NSURLSessionDataDelegate, NSURLSessionDownloadDelegate> {
|
||||
const cc::network::DownloaderApple *_outer;
|
||||
cc::network::DownloaderHints _hints;
|
||||
ccstd::queue<NSURLSessionTask *> _taskQueue;
|
||||
}
|
||||
@property (nonatomic, strong) NSURLSession *downloadSession;
|
||||
@property (nonatomic, strong) NSMutableDictionary *taskDict; // ocTask: DownloadTaskWrapper
|
||||
|
||||
- (id)init:(const cc::network::DownloaderApple *)o hints:(const cc::network::DownloaderHints &)hints;
|
||||
- (const cc::network::DownloaderHints &)getHints;
|
||||
- (NSURLSessionDataTask *)createDataTask:(std::shared_ptr<const cc::network::DownloadTask> &)task;
|
||||
- (NSURLSessionDownloadTask *)createDownloadTask:(std::shared_ptr<const cc::network::DownloadTask> &)task;
|
||||
- (void)saveResumeData:(NSData *)resumeData forTaskStoragePath:(const ccstd::string &) path;
|
||||
- (void)abort:(NSURLSessionTask *)task;
|
||||
- (void)doDestroy;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// C++ Classes Implementation
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
struct DownloadTaskApple : public IDownloadTask {
|
||||
DownloadTaskApple()
|
||||
: dataTask(nil),
|
||||
downloadTask(nil) {
|
||||
DLLOG("Construct DownloadTaskApple %p", this);
|
||||
}
|
||||
|
||||
virtual ~DownloadTaskApple() {
|
||||
DLLOG("Destruct DownloadTaskApple %p", this);
|
||||
}
|
||||
NSURLSessionDataTask *dataTask;
|
||||
NSURLSessionDownloadTask *downloadTask;
|
||||
};
|
||||
#define DeclareDownloaderImplVar DownloaderAppleImpl *impl = (__bridge DownloaderAppleImpl *)_impl
|
||||
// the _impl's type is id, we should convert it to subclass before call it's methods
|
||||
DownloaderApple::DownloaderApple(const DownloaderHints &hints)
|
||||
: _impl(nil) {
|
||||
DLLOG("Construct DownloaderApple %p", this);
|
||||
_impl = (__bridge void *)[[DownloaderAppleImpl alloc] init:this hints:hints];
|
||||
}
|
||||
|
||||
DownloaderApple::~DownloaderApple() {
|
||||
DeclareDownloaderImplVar;
|
||||
[impl doDestroy];
|
||||
DLLOG("Destruct DownloaderApple %p", this);
|
||||
}
|
||||
IDownloadTask *DownloaderApple::createCoTask(std::shared_ptr<const DownloadTask> &task) {
|
||||
DownloadTaskApple *coTask = ccnew DownloadTaskApple();
|
||||
DeclareDownloaderImplVar;
|
||||
if (task->storagePath.length()) {
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
NSString *requesetURL = [NSString stringWithFormat:@"%s", task->requestURL.c_str()];
|
||||
NSString *savaPath = [NSString stringWithFormat:@"%s", (cc::FileUtils::getInstance()->getWritablePath() + "resumeData.plist").c_str()];
|
||||
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:savaPath];
|
||||
NSString *hasResumeData = [dict objectForKey:requesetURL];
|
||||
if ([hasResumeData isEqual: @"YES"]) {
|
||||
CC_CURRENT_ENGINE()->getScheduler()->schedule([=](float dt) mutable {
|
||||
coTask->downloadTask = [impl createDownloadTask:task];
|
||||
},this , 0, 0, 0.1F, false, task->requestURL);
|
||||
} else {
|
||||
coTask->downloadTask = [impl createDownloadTask:task];
|
||||
}
|
||||
#else
|
||||
coTask->downloadTask = [impl createDownloadTask:task];
|
||||
#endif
|
||||
} else {
|
||||
coTask->dataTask = [impl createDataTask:task];
|
||||
}
|
||||
return coTask;
|
||||
}
|
||||
void DownloaderApple::abort(const std::unique_ptr<IDownloadTask> &task) {
|
||||
DLLOG("DownloaderApple:abort");
|
||||
DeclareDownloaderImplVar;
|
||||
cc::network::DownloadTaskApple *downloadTask = (cc::network::DownloadTaskApple *)task.get();
|
||||
NSURLSessionTask *taskOC = downloadTask->dataTask ? downloadTask->dataTask : downloadTask->downloadTask;
|
||||
[impl abort:taskOC];
|
||||
}
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// OC Classes Implementation
|
||||
@implementation DownloadTaskWrapper
|
||||
|
||||
- (id)init:(std::shared_ptr<const cc::network::DownloadTask> &)t {
|
||||
DLLOG("Construct DonloadTaskWrapper %p", self);
|
||||
_dataArray = [NSMutableArray arrayWithCapacity:8];
|
||||
[_dataArray retain];
|
||||
_task = t;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (const cc::network::DownloadTask *)get {
|
||||
return _task.get();
|
||||
}
|
||||
|
||||
- (void)addData:(NSData *)data {
|
||||
[_dataArray addObject:data];
|
||||
self.bytesReceived += static_cast<uint32_t>(data.length);
|
||||
self.totalBytesReceived += static_cast<uint32_t>(data.length);
|
||||
}
|
||||
|
||||
- (uint32_t)transferDataToBuffer:(void *)buffer lengthOfBuffer:(uint32_t)len {
|
||||
uint32_t bytesReceived = 0;
|
||||
int receivedDataObject = 0;
|
||||
|
||||
__block char *p = (char *)buffer;
|
||||
for (NSData *data in _dataArray) {
|
||||
// check
|
||||
if (bytesReceived + data.length > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
// copy data
|
||||
[data enumerateByteRangesUsingBlock:^(const void *bytes,
|
||||
NSRange byteRange,
|
||||
BOOL *stop) {
|
||||
memcpy(p, bytes, byteRange.length);
|
||||
p += byteRange.length;
|
||||
*stop = NO;
|
||||
}];
|
||||
|
||||
// accumulate
|
||||
bytesReceived += data.length;
|
||||
++receivedDataObject;
|
||||
}
|
||||
|
||||
// remove receivedNSDataObject from dataArray
|
||||
[_dataArray removeObjectsInRange:NSMakeRange(0, receivedDataObject)];
|
||||
self.bytesReceived -= bytesReceived;
|
||||
return bytesReceived;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_dataArray release];
|
||||
[super dealloc];
|
||||
DLLOG("Destruct DownloadTaskWrapper %p", self);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation DownloaderAppleImpl
|
||||
- (id)init:(const cc::network::DownloaderApple *)o hints:(const cc::network::DownloaderHints &)hints {
|
||||
DLLOG("Construct DownloaderAppleImpl %p", self);
|
||||
// save outer task ref
|
||||
_outer = o;
|
||||
_hints = hints;
|
||||
// create task dictionary
|
||||
self.taskDict = [NSMutableDictionary dictionary];
|
||||
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
// create backgroundSession for iOS to support background download
|
||||
self.downloadSession = [self backgroundURLSession];
|
||||
|
||||
// cancel and save last running tasks
|
||||
[self.downloadSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
|
||||
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
|
||||
// cancelTask with resume Data
|
||||
if ((long)downloadTask.state == 0) {
|
||||
[(NSURLSessionDownloadTask *)downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
|
||||
if(downloadTask.originalRequest.URL.absoluteString) {
|
||||
NSString *tempFilePathWithHash = [NSString stringWithFormat:@"%s%lu%s", cc::FileUtils::getInstance()->getWritablePath().c_str(), (unsigned long)[downloadTask.originalRequest.URL.absoluteString hash], _hints.tempFileNameSuffix.c_str()];
|
||||
[resumeData writeToFile:tempFilePathWithHash atomically:YES];
|
||||
} else if (downloadTask.currentRequest.URL.absoluteString) {
|
||||
NSString *tempFilePathWithHash = [NSString stringWithFormat:@"%s%lu%s", cc::FileUtils::getInstance()->getWritablePath().c_str(), (unsigned long)[downloadTask.currentRequest.URL.absoluteString hash], _hints.tempFileNameSuffix.c_str()];
|
||||
[resumeData writeToFile:tempFilePathWithHash atomically:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}];
|
||||
#else
|
||||
NSURLSessionConfiguration *defaultConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
self.downloadSession = [NSURLSession sessionWithConfiguration:defaultConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
|
||||
#endif
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - backgroundURLSession
|
||||
- (NSURLSession *)backgroundURLSession {
|
||||
// use for backgroundSession global unique identifier, remove it if use singleton instance in the future.
|
||||
static int sessionId = 0;
|
||||
// Single thread delegate mode
|
||||
// NSOperationQueue *queue = [[NSOperationQueue alloc] init];
|
||||
// queue.maxConcurrentOperationCount = 1;
|
||||
NSString* identifierStr = [NSString stringWithFormat:@"%s%d", "BackgroundDownloadIdentifier" , sessionId];
|
||||
NSURLSessionConfiguration *backgroudConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifierStr];
|
||||
sessionId++;
|
||||
return [NSURLSession sessionWithConfiguration:backgroudConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
|
||||
}
|
||||
|
||||
- (const cc::network::DownloaderHints &)getHints {
|
||||
return _hints;
|
||||
}
|
||||
|
||||
- (NSURLSessionDataTask *)createDataTask:(std::shared_ptr<const cc::network::DownloadTask> &)task {
|
||||
const char *urlStr = task->requestURL.c_str();
|
||||
DLLOG("DownloaderAppleImpl createDataTask: %s", urlStr);
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlStr]];
|
||||
NSURLRequest *request = nil;
|
||||
if (_hints.timeoutInSeconds > 0) {
|
||||
request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:(NSTimeInterval)_hints.timeoutInSeconds];
|
||||
} else {
|
||||
request = [NSURLRequest requestWithURL:url];
|
||||
}
|
||||
NSURLSessionDataTask *ocTask = [self.downloadSession dataTaskWithRequest:request];
|
||||
DownloadTaskWrapper *taskWrapper = [[DownloadTaskWrapper alloc] init:task];
|
||||
[self.taskDict setObject:taskWrapper forKey:ocTask];
|
||||
[taskWrapper release];
|
||||
|
||||
if (_taskQueue.size() < _hints.countOfMaxProcessingTasks) {
|
||||
[ocTask resume];
|
||||
_taskQueue.push(nil);
|
||||
} else {
|
||||
_taskQueue.push(ocTask);
|
||||
}
|
||||
return ocTask;
|
||||
};
|
||||
|
||||
- (NSURLSessionDownloadTask *)createDownloadTask:(std::shared_ptr<const cc::network::DownloadTask> &)task {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
NSString *requesetURL = [NSString stringWithFormat:@"%s", task->requestURL.c_str()];
|
||||
[dict setObject:@"YES" forKey:requesetURL];
|
||||
NSString *savaPath = [NSString stringWithFormat:@"%s", (cc::FileUtils::getInstance()->getWritablePath() + "resumeData.plist").c_str()];
|
||||
[dict writeToFile:savaPath atomically:YES];
|
||||
|
||||
const char *urlStr = task->requestURL.c_str();
|
||||
DLLOG("DownloaderAppleImpl createDownloadTask: %s", urlStr);
|
||||
NSString *resourcePath = [NSString stringWithUTF8String:urlStr];
|
||||
NSString *encodePath = [resourcePath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];
|
||||
NSURL *url = [NSURL URLWithString:encodePath];
|
||||
NSMutableURLRequest *request = nil;
|
||||
if (_hints.timeoutInSeconds > 0) {
|
||||
request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:(NSTimeInterval)_hints.timeoutInSeconds];
|
||||
} else {
|
||||
request = [NSMutableURLRequest requestWithURL:url];
|
||||
}
|
||||
for (auto it = task->header.begin(); it != task->header.end(); ++it) {
|
||||
NSString *keyStr = [NSString stringWithUTF8String:it->first.c_str()];
|
||||
NSString *valueStr = [NSString stringWithUTF8String:it->second.c_str()];
|
||||
[request setValue:valueStr forHTTPHeaderField:keyStr];
|
||||
}
|
||||
NSString *tempFilePath = [NSString stringWithFormat:@"%s%s", task->storagePath.c_str(), _hints.tempFileNameSuffix.c_str()];
|
||||
NSData *resumeData = [NSData dataWithContentsOfFile:tempFilePath];
|
||||
|
||||
NSString *tempFilePathWithHash = [NSString stringWithFormat:@"%s%lu%s", cc::FileUtils::getInstance()->getWritablePath().c_str(), (unsigned long)[url hash], _hints.tempFileNameSuffix.c_str()];
|
||||
NSData *resumeData2 = [NSData dataWithContentsOfFile:tempFilePathWithHash];
|
||||
|
||||
NSURLSessionDownloadTask *ocTask = nil;
|
||||
if (resumeData && task->header.size() <= 0) {
|
||||
ocTask = [self.downloadSession downloadTaskWithResumeData:resumeData];
|
||||
} else if (resumeData2 && task->header.size() <= 0) {
|
||||
ocTask = [self.downloadSession downloadTaskWithResumeData:resumeData2];
|
||||
} else {
|
||||
ocTask = [self.downloadSession downloadTaskWithRequest:request];
|
||||
}
|
||||
|
||||
DownloadTaskWrapper *taskWrapper = [[DownloadTaskWrapper alloc] init:task];
|
||||
[self.taskDict setObject:taskWrapper forKey:ocTask];
|
||||
[taskWrapper release];
|
||||
|
||||
if (_taskQueue.size() < _hints.countOfMaxProcessingTasks) {
|
||||
[ocTask resume];
|
||||
_taskQueue.push(nil);
|
||||
} else {
|
||||
_taskQueue.push(ocTask);
|
||||
}
|
||||
return ocTask;
|
||||
};
|
||||
|
||||
- (void)saveResumeData:(NSData *)resumeData forTaskStoragePath:(const ccstd::string &) path {
|
||||
NSString *tempFilePath = [NSString stringWithFormat:@"%s%s", path.c_str(), _hints.tempFileNameSuffix.c_str()];
|
||||
NSString *tempFileDir = [tempFilePath stringByDeletingLastPathComponent];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
BOOL isDir = false;
|
||||
if ([fileManager fileExistsAtPath:tempFileDir isDirectory:&isDir]) {
|
||||
if (NO == isDir) {
|
||||
// REFINE: the directory is a file, not a directory, how to echo to developer?
|
||||
DLLOG("DownloaderAppleImpl temp dir is a file!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
NSURL *tempFileURL = [NSURL fileURLWithPath:tempFileDir];
|
||||
if (NO == [fileManager createDirectoryAtURL:tempFileURL withIntermediateDirectories:YES attributes:nil error:nil]) {
|
||||
// create directory failed
|
||||
DLLOG("DownloaderAppleImpl create temp dir failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
[resumeData writeToFile:tempFilePath atomically:YES];
|
||||
}
|
||||
|
||||
- (void)abort:(NSURLSessionTask *)task {
|
||||
// cancel all download task
|
||||
NSEnumerator *enumeratorKey = [self.taskDict keyEnumerator];
|
||||
for (NSURLSessionTask *taskKey in enumeratorKey) {
|
||||
if (task != taskKey) {
|
||||
continue;
|
||||
}
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:taskKey];
|
||||
|
||||
// no resume support for a data task
|
||||
ccstd::string storagePath = [wrapper get]->storagePath;
|
||||
if (storagePath.length() == 0) {
|
||||
[taskKey cancel];
|
||||
} else {
|
||||
[(NSURLSessionDownloadTask *)taskKey cancelByProducingResumeData:^(NSData *resumeData) {
|
||||
if (resumeData) {
|
||||
[self saveResumeData:resumeData forTaskStoragePath:storagePath];
|
||||
}
|
||||
}];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doDestroy {
|
||||
// cancel all download task
|
||||
NSEnumerator *enumeratorKey = [self.taskDict keyEnumerator];
|
||||
for (NSURLSessionDownloadTask *task in enumeratorKey) {
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:task];
|
||||
|
||||
// no resume support for a data task
|
||||
ccstd::string storagePath = [wrapper get]->storagePath;
|
||||
if (storagePath.length() == 0) {
|
||||
[task cancel];
|
||||
} else {
|
||||
[task cancelByProducingResumeData:^(NSData *resumeData) {
|
||||
if (resumeData) {
|
||||
[self saveResumeData:resumeData forTaskStoragePath:storagePath];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
_outer = nullptr;
|
||||
|
||||
while (!_taskQueue.empty())
|
||||
_taskQueue.pop();
|
||||
|
||||
[self.downloadSession invalidateAndCancel];
|
||||
[self release];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
DLLOG("Destruct DownloaderAppleImpl %p", self);
|
||||
self.downloadSession = nil;
|
||||
self.taskDict = nil;
|
||||
[super dealloc];
|
||||
}
|
||||
#pragma mark - NSURLSessionTaskDelegate methods
|
||||
|
||||
//@optional
|
||||
|
||||
/* An HTTP request is attempting to perform a redirection to a different
|
||||
* URL. You must invoke the completion routine to allow the
|
||||
* redirection, allow the redirection with a modified request, or
|
||||
* pass nil to the completionHandler to cause the body of the redirection
|
||||
* response to be delivered as the payload of this request. The default
|
||||
* is to follow redirections.
|
||||
*
|
||||
* For tasks in background sessions, redirections will always be followed and this method will not be called.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
//willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
// newRequest:(NSURLRequest *)request
|
||||
// completionHandler:(void (^)(NSURLRequest *))completionHandler;
|
||||
|
||||
/* The task has received a request specific authentication challenge.
|
||||
* If this delegate is not implemented, the session specific authentication challenge
|
||||
* will *NOT* be called and the behavior will be the same as using the default handling
|
||||
* disposition.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
//didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
// completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
|
||||
|
||||
/* Sent if a task requires a new, unopened body stream. This may be
|
||||
* necessary when authentication has failed for any request that
|
||||
* involves a body stream.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
// needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler;
|
||||
|
||||
/* Sent periodically to notify the delegate of upload progress. This
|
||||
* information is also available as properties of the task.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session task :(NSURLSessionTask *)task
|
||||
// didSendBodyData:(uint32_t)bytesSent
|
||||
// totalBytesSent:(uint32_t)totalBytesSent
|
||||
// totalBytesExpectedToSend:(uint32_t)totalBytesExpectedToSend;
|
||||
|
||||
/* Sent as the last message related to a specific task. Error may be
|
||||
* nil, which implies that no error occurred and this task is complete.
|
||||
*/
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(NSError *)error {
|
||||
DLLOG("DownloaderAppleImpl task: \"%s\" didCompleteWithError: %d errDesc: %s", [task.originalRequest.URL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], (error ? static_cast<int32_t>(error.code) : 0), [error.localizedDescription cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
|
||||
// clean wrapper C++ object
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:task];
|
||||
|
||||
if (wrapper) {
|
||||
if (_outer) {
|
||||
if (error) {
|
||||
int errorCode = static_cast<int32_t>(error.code);
|
||||
NSString *errorMsg = error.localizedDescription;
|
||||
if (error.code == NSURLErrorCancelled) {
|
||||
//cancel
|
||||
errorCode = cc::network::DownloadTask::ERROR_ABORT;
|
||||
errorMsg = @"downloadFile:abort";
|
||||
}
|
||||
ccstd::vector<unsigned char> buf; // just a placeholder
|
||||
_outer->onTaskFinish(*[wrapper get],
|
||||
cc::network::DownloadTask::ERROR_IMPL_INTERNAL,
|
||||
errorCode,
|
||||
[errorMsg cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
buf);
|
||||
} else if (![wrapper get]->storagePath.length()) {
|
||||
// call onTaskFinish for a data task
|
||||
// (for a file download task, callback is called in didFinishDownloadingToURL)
|
||||
ccstd::string errorString;
|
||||
|
||||
const uint32_t buflen = [wrapper totalBytesReceived];
|
||||
ccstd::vector<unsigned char> data((size_t)buflen);
|
||||
char *buf = (char *)data.data();
|
||||
[wrapper transferDataToBuffer:buf lengthOfBuffer:buflen];
|
||||
|
||||
_outer->onTaskFinish(*[wrapper get],
|
||||
cc::network::DownloadTask::ERROR_NO_ERROR,
|
||||
0,
|
||||
errorString,
|
||||
data);
|
||||
} else {
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)task.response).statusCode;
|
||||
|
||||
// Check for error status code
|
||||
if (statusCode >= 400) {
|
||||
ccstd::vector<unsigned char> buf; // just a placeholder
|
||||
const char *originalURL = [task.originalRequest.URL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
ccstd::string errorMessage = cc::StringUtils::format("Downloader: Failed to download %s with status code (%d)", originalURL, static_cast<int32_t>(statusCode));
|
||||
|
||||
_outer->onTaskFinish(*[wrapper get],
|
||||
cc::network::DownloadTask::ERROR_IMPL_INTERNAL,
|
||||
0,
|
||||
errorMessage,
|
||||
buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
|
||||
NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
|
||||
|
||||
if([error.userInfo objectForKey:NSURLErrorFailingURLStringErrorKey]) {
|
||||
NSString *url = [error.userInfo objectForKey:NSURLErrorFailingURLStringErrorKey];
|
||||
NSLog(@"url:%@",url);
|
||||
NSString *tempFilePath = [NSString stringWithFormat:@"%s%lu%s", cc::FileUtils::getInstance()->getWritablePath().c_str(), (unsigned long)[url hash], _hints.tempFileNameSuffix.c_str()];
|
||||
[resumeData writeToFile:tempFilePath atomically:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
[self.taskDict removeObjectForKey:task];
|
||||
|
||||
while (!_taskQueue.empty() && _taskQueue.front() == nil) {
|
||||
_taskQueue.pop();
|
||||
}
|
||||
if (!_taskQueue.empty()) {
|
||||
[_taskQueue.front() resume];
|
||||
_taskQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - NSURLSessionDataDelegate methods
|
||||
//@optional
|
||||
/* The task has received a response and no further messages will be
|
||||
* received until the completion block is called. The disposition
|
||||
* allows you to cancel a request or to turn a data task into a
|
||||
* download task. This delegate message is optional - if you do not
|
||||
* implement it, you can get the response as a property of the task.
|
||||
*
|
||||
* This method will not be called for background upload tasks (which cannot be converted to download tasks).
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session dataTask :(NSURLSessionDataTask *)dataTask
|
||||
// didReceiveResponse:(NSURLResponse *)response
|
||||
// completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
|
||||
//{
|
||||
// DLLOG("DownloaderAppleImpl dataTask: response:%s", [response.description cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
// completionHandler(NSURLSessionResponseAllow);
|
||||
//}
|
||||
|
||||
/* Notification that a data task has become a download task. No
|
||||
* future messages will be sent to the data task.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
//didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask;
|
||||
|
||||
/* Sent when data is available for the delegate to consume. It is
|
||||
* assumed that the delegate will retain and not copy the data. As
|
||||
* the data may be discontiguous, you should use
|
||||
* [NSData enumerateByteRangesUsingBlock:] to access it.
|
||||
*/
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data {
|
||||
DLLOG("DownloaderAppleImpl dataTask: \"%s\" didReceiveDataLen %d",
|
||||
[dataTask.originalRequest.URL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
static_cast<int32_t>(data.length));
|
||||
if (nullptr == _outer) {
|
||||
return;
|
||||
}
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:dataTask];
|
||||
[wrapper addData:data];
|
||||
|
||||
std::function<uint32_t(void *, uint32_t)> transferDataToBuffer =
|
||||
[wrapper](void *buffer, uint32_t bufLen) -> uint32_t {
|
||||
return [wrapper transferDataToBuffer:buffer lengthOfBuffer:bufLen];
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
_outer->onTaskProgress(*[wrapper get],
|
||||
wrapper.bytesReceived,
|
||||
wrapper.totalBytesReceived,
|
||||
static_cast<uint32_t>(dataTask.countOfBytesExpectedToReceive),
|
||||
transferDataToBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/* Invoke the completion routine with a valid NSCachedURLResponse to
|
||||
* allow the resulting data to be cached, or pass nil to prevent
|
||||
* caching. Note that there is no guarantee that caching will be
|
||||
* attempted for a given resource, and you should not rely on this
|
||||
* message to receive the resource data.
|
||||
*/
|
||||
//- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
// willCacheResponse:(NSCachedURLResponse *)proposedResponse
|
||||
// completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler;
|
||||
|
||||
#pragma mark - NSURLSessionDownloadDelegate methods
|
||||
|
||||
/* Sent when a download task that has completed a download. The delegate should
|
||||
* copy or move the file at the given location to a new location as it will be
|
||||
* removed when the delegate message returns. URLSession:task:didCompleteWithError: will
|
||||
* still be called.
|
||||
*/
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
didFinishDownloadingToURL:(NSURL *)location {
|
||||
DLLOG("DownloaderAppleImpl downloadTask: \"%s\" didFinishDownloadingToURL %s",
|
||||
[downloadTask.originalRequest.URL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
[location.absoluteString cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
if (nullptr == _outer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On iOS 9 a response with status code 4xx(Client Error) or 5xx(Server Error)
|
||||
// might end up calling this delegate method, saving the error message to the storage path
|
||||
// and treating this download task as a successful one, so we need to check the status code here
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)downloadTask.response).statusCode;
|
||||
if (statusCode >= 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:downloadTask];
|
||||
if (wrapper) {
|
||||
const char *storagePath = [wrapper get]->storagePath.c_str();
|
||||
NSString *destPath = [NSString stringWithUTF8String:storagePath];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSURL *destURL = nil;
|
||||
|
||||
do {
|
||||
if ([destPath hasPrefix:@"file://"]) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ('/' == [destPath characterAtIndex:0]) {
|
||||
destURL = [NSURL fileURLWithPath:destPath];
|
||||
break;
|
||||
}
|
||||
|
||||
// relative path, store to user domain default
|
||||
NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
||||
NSURL *documentsDirectory = URLs[0];
|
||||
destURL = [documentsDirectory URLByAppendingPathComponent:destPath];
|
||||
} while (0);
|
||||
|
||||
// Make sure we overwrite anything that's already there
|
||||
[fileManager removeItemAtURL:destURL error:NULL];
|
||||
|
||||
// copy file to dest location
|
||||
int errorCode = cc::network::DownloadTask::ERROR_NO_ERROR;
|
||||
int errorCodeInternal = 0;
|
||||
ccstd::string errorString;
|
||||
|
||||
NSError *error = nil;
|
||||
if ([fileManager copyItemAtURL:location toURL:destURL error:&error]) {
|
||||
// success, remove temp file if it exist
|
||||
if (_hints.tempFileNameSuffix.length()) {
|
||||
NSString *tempStr = [[destURL absoluteString] stringByAppendingFormat:@"%s", _hints.tempFileNameSuffix.c_str()];
|
||||
NSURL *tempDestUrl = [NSURL URLWithString:tempStr];
|
||||
[fileManager removeItemAtURL:tempDestUrl error:NULL];
|
||||
}
|
||||
} else {
|
||||
errorCode = cc::network::DownloadTask::ERROR_FILE_OP_FAILED;
|
||||
if (error) {
|
||||
errorCodeInternal = static_cast<int32_t>(error.code);
|
||||
errorString = [error.localizedDescription cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
}
|
||||
ccstd::vector<unsigned char> buf; // just a placeholder
|
||||
_outer->onTaskFinish(*[wrapper get], errorCode, errorCodeInternal, errorString, buf);
|
||||
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
NSString *requesetURL = [NSString stringWithFormat:@"%s", [wrapper get]->requestURL.c_str()];
|
||||
[dict removeObjectForKey:requesetURL];
|
||||
NSString *savaPath = [NSString stringWithFormat:@"%s", (cc::FileUtils::getInstance()->getWritablePath() + "resumeData.plist").c_str()];
|
||||
[dict writeToFile:savaPath atomically:YES];
|
||||
}
|
||||
}
|
||||
|
||||
// @optional
|
||||
/* Sent periodically to notify the delegate of download progress. */
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
didWriteData:(int64_t)bytesWritten
|
||||
totalBytesWritten:(int64_t)totalBytesWritten
|
||||
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
||||
// NSLog(@"DownloaderAppleImpl downloadTask: \"%@\" received: %lld total: %lld", downloadTask.originalRequest.URL, totalBytesWritten, totalBytesExpectedToWrite);
|
||||
|
||||
if (nullptr == _outer) {
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadTaskWrapper *wrapper = [self.taskDict objectForKey:downloadTask];
|
||||
std::function<uint32_t(void *, uint32_t)> transferDataToBuffer; // just a placeholder
|
||||
if (wrapper) {
|
||||
_outer->onTaskProgress(*[wrapper get], static_cast<uint32_t>(bytesWritten), static_cast<uint32_t>(totalBytesWritten), static_cast<uint32_t>(totalBytesExpectedToWrite), transferDataToBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sent when a download has been resumed. If a download failed with an
|
||||
* error, the -userInfo dictionary of the error will contain an
|
||||
* NSURLSessionDownloadTaskResumeData key, whose value is the resume
|
||||
* data.
|
||||
*/
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
didResumeAtOffset:(int64_t)fileOffset
|
||||
expectedTotalBytes:(int64_t)expectedTotalBytes {
|
||||
NSLog(@"[REFINE]DownloaderAppleImpl downloadTask: \"%@\" didResumeAtOffset: %lld", downloadTask.originalRequest.URL, fileOffset);
|
||||
}
|
||||
|
||||
@end
|
||||
78
cocos/network/DownloaderImpl.h
Normal file
78
cocos/network/DownloaderImpl.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include "base/Log.h"
|
||||
#include "base/Macros.h"
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/unordered_map.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
//#define CC_DOWNLOADER_DEBUG
|
||||
#ifdef CC_DOWNLOADER_DEBUG
|
||||
#define DLLOG(format, ...) CC_LOG_DEBUG(format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define DLLOG(...) \
|
||||
do { \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
class DownloadTask;
|
||||
|
||||
class CC_DLL IDownloadTask {
|
||||
public:
|
||||
virtual ~IDownloadTask() = default;
|
||||
};
|
||||
|
||||
class IDownloaderImpl {
|
||||
public:
|
||||
virtual ~IDownloaderImpl() = default;
|
||||
|
||||
std::function<void(const DownloadTask &task,
|
||||
uint32_t bytesReceived,
|
||||
uint32_t totalBytesReceived,
|
||||
uint32_t totalBytesExpected,
|
||||
std::function<uint32_t(void *buffer, uint32_t len)> &transferDataToBuffer)>
|
||||
onTaskProgress;
|
||||
|
||||
std::function<void(const DownloadTask &task,
|
||||
int errorCode,
|
||||
int errorCodeInternal,
|
||||
const ccstd::string &errorStr,
|
||||
const ccstd::vector<unsigned char> &data)>
|
||||
onTaskFinish;
|
||||
|
||||
virtual IDownloadTask *createCoTask(std::shared_ptr<const DownloadTask> &task) = 0;
|
||||
|
||||
virtual void abort(const std::unique_ptr<IDownloadTask> &task) = 0;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
70
cocos/network/HttpAsynConnection-apple.h
Normal file
70
cocos/network/HttpAsynConnection-apple.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef __HTTPASYNCONNECTION_H__
|
||||
#define __HTTPASYNCONNECTION_H__
|
||||
/// @cond DO_NOT_SHOW
|
||||
|
||||
#if (CC_PLATFORM == CC_PLATFORM_MACOS) || (CC_PLATFORM == CC_PLATFORM_IOS)
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
/// @cond
|
||||
@interface HttpAsynConnection : NSObject <NSURLSessionDelegate> {
|
||||
NSURLSession *session;
|
||||
}
|
||||
|
||||
// The original URL to download. Due to redirects the actual content may come from another URL
|
||||
@property (strong) NSString *srcURL;
|
||||
|
||||
@property (strong) NSString *sslFile;
|
||||
|
||||
@property (copy) NSDictionary *responseHeader;
|
||||
|
||||
@property (strong) NSMutableData *responseData;
|
||||
|
||||
@property (readonly) NSInteger getDataTime;
|
||||
|
||||
@property (readonly) NSInteger responseCode;
|
||||
@property (readonly) NSString *statusString;
|
||||
|
||||
@property (strong) NSError *responseError;
|
||||
@property (strong) NSError *connError;
|
||||
|
||||
@property (strong) NSURLSessionDataTask *task;
|
||||
|
||||
@property bool finish;
|
||||
|
||||
@property (strong) NSRunLoop *runLoop;
|
||||
|
||||
// instructs the class to start the request.
|
||||
- (void)startRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
#endif // #if (CC_PLATFORM == CC_PLATFORM_MACOS) || (CC_PLATFORM == CC_PLATFORM_IOS)
|
||||
|
||||
/// @endcond
|
||||
#endif //__HTTPASYNCONNECTION_H__
|
||||
184
cocos/network/HttpAsynConnection-apple.m
Executable file
184
cocos/network/HttpAsynConnection-apple.m
Executable file
@@ -0,0 +1,184 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2013-2017 Chukong Technologies Inc.
|
||||
|
||||
http://www.cocos2d-x.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#if (CC_PLATFORM == CC_PLATFORM_MACOS) || (CC_PLATFORM == CC_PLATFORM_IOS)
|
||||
|
||||
#import "network/HttpAsynConnection-apple.h"
|
||||
|
||||
@interface HttpAsynConnection ()
|
||||
|
||||
@property (readwrite) NSString *statusString;
|
||||
|
||||
- (BOOL)shouldTrustProtectionSpace:(NSURLProtectionSpace *)protectionSpace;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpAsynConnection
|
||||
|
||||
@synthesize srcURL = srcURL;
|
||||
@synthesize sslFile = sslFile;
|
||||
@synthesize responseHeader = responseHeader;
|
||||
@synthesize responseData = responseData;
|
||||
@synthesize getDataTime = getDataTime;
|
||||
@synthesize responseCode = responseCode;
|
||||
@synthesize statusString = statusString;
|
||||
@synthesize responseError = responseError;
|
||||
@synthesize connError = connError;
|
||||
@synthesize task = task;
|
||||
@synthesize finish = finish;
|
||||
@synthesize runLoop = runLoop;
|
||||
|
||||
- (void)dealloc {
|
||||
[srcURL release];
|
||||
[sslFile release];
|
||||
[responseHeader release];
|
||||
[responseData release];
|
||||
[responseError release];
|
||||
[runLoop release];
|
||||
[connError release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)startRequest:(NSURLRequest *)request {
|
||||
#ifdef CC_DEBUG
|
||||
// NSLog(@"Starting to load %@", srcURL);
|
||||
#endif
|
||||
|
||||
finish = false;
|
||||
|
||||
self.responseData = [NSMutableData data];
|
||||
getDataTime = 0;
|
||||
|
||||
self.responseError = nil;
|
||||
self.connError = nil;
|
||||
|
||||
session = [NSURLSession sharedSession];
|
||||
|
||||
task = [session dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *_Nullable error) {
|
||||
if (error != nil) {
|
||||
self.connError = error;
|
||||
finish = true;
|
||||
return;
|
||||
}
|
||||
if (response != nil) {
|
||||
#ifdef CC_DEBUG
|
||||
// NSLog(@"Received response from request to url %@", srcURL);
|
||||
#endif
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
//NSLog(@"All headers = %@", [httpResponse allHeaderFields]);
|
||||
self.responseHeader = [httpResponse allHeaderFields];
|
||||
|
||||
responseCode = httpResponse.statusCode;
|
||||
self.statusString = [NSHTTPURLResponse localizedStringForStatusCode:responseCode];
|
||||
if (responseCode == 200)
|
||||
self.statusString = @"OK";
|
||||
|
||||
/*The individual values of the numeric status codes defined for HTTP/1.1
|
||||
| "200" ; OK
|
||||
| "201" ; Created
|
||||
| "202" ; Accepted
|
||||
| "203" ; Non-Authoritative Information
|
||||
| "204" ; No Content
|
||||
| "205" ; Reset Content
|
||||
| "206" ; Partial Content
|
||||
*/
|
||||
if (responseCode < 200 || responseCode >= 300) { // something went wrong, abort the whole thing
|
||||
self.responseError = [NSError errorWithDomain:@"CCBackendDomain"
|
||||
code:responseCode
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Bad HTTP Response Code"}];
|
||||
}
|
||||
|
||||
[responseData setLength:0];
|
||||
}
|
||||
|
||||
if (data != nil) {
|
||||
[responseData appendData:data];
|
||||
getDataTime++;
|
||||
}
|
||||
|
||||
finish = true;
|
||||
}];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
//Server evaluates client's certificate
|
||||
- (BOOL)shouldTrustProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
|
||||
if (sslFile == nil)
|
||||
return YES;
|
||||
//load the bundle client certificate
|
||||
NSString *certPath = [[NSBundle mainBundle] pathForResource:sslFile ofType:@"der"];
|
||||
NSData *certData = [[NSData alloc] initWithContentsOfFile:certPath];
|
||||
CFDataRef certDataRef = (CFDataRef)certData;
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, certDataRef);
|
||||
|
||||
//Establish a chain of trust anchored on our bundled certificate
|
||||
CFArrayRef certArrayRef = CFArrayCreate(NULL, (void *)&cert, 1, NULL);
|
||||
SecTrustRef serverTrust = protectionSpace.serverTrust;
|
||||
SecTrustSetAnchorCertificates(serverTrust, certArrayRef);
|
||||
|
||||
//Verify that trust
|
||||
SecTrustResultType trustResult;
|
||||
SecTrustEvaluate(serverTrust, &trustResult);
|
||||
|
||||
if (trustResult == kSecTrustResultRecoverableTrustFailure) {
|
||||
CFDataRef errDataRef = SecTrustCopyExceptions(serverTrust);
|
||||
SecTrustSetExceptions(serverTrust, errDataRef);
|
||||
SecTrustEvaluate(serverTrust, &trustResult);
|
||||
CFRelease(errDataRef);
|
||||
}
|
||||
[certData release];
|
||||
if (cert) {
|
||||
CFRelease(cert);
|
||||
}
|
||||
if (certArrayRef) {
|
||||
CFRelease(certArrayRef);
|
||||
}
|
||||
//Did our custom trust chain evaluate successfully?
|
||||
return trustResult == kSecTrustResultUnspecified || trustResult == kSecTrustResultProceed;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler {
|
||||
id<NSURLAuthenticationChallengeSender> sender = challenge.sender;
|
||||
NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;
|
||||
|
||||
//Should server trust client?
|
||||
if ([self shouldTrustProtectionSpace:protectionSpace]) {
|
||||
SecTrustRef trust = [protectionSpace serverTrust];
|
||||
//
|
||||
// SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, 0);
|
||||
//
|
||||
// NSData *serverCertificateData = (NSData*)SecCertificateCopyData(certificate);
|
||||
// NSString *serverCertificateDataHash = [[serverCertificateData base64EncodedString] ]
|
||||
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||
} else {
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // #if (CC_PLATFORM == CC_PLATFORM_MACOS) || (CC_PLATFORM == CC_PLATFORM_IOS)
|
||||
529
cocos/network/HttpClient-apple.mm
Normal file
529
cocos/network/HttpClient-apple.mm
Normal file
@@ -0,0 +1,529 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2012 greathqy
|
||||
Copyright (c) 2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/HttpClient.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#import "network/HttpAsynConnection-apple.h"
|
||||
#include "network/HttpCookie.h"
|
||||
#include "platform/FileUtils.h"
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "base/ThreadPool.h"
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
static HttpClient *_httpClient = nullptr; // pointer to singleton
|
||||
static LegacyThreadPool *gThreadPool = nullptr;
|
||||
|
||||
static int processTask(HttpClient *client, HttpRequest *request, NSString *requestType, void *stream, long *errorCode, void *headerStream, char *errorBuffer);
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThread() {
|
||||
increaseThreadCount();
|
||||
|
||||
while (true) @autoreleasepool {
|
||||
HttpRequest *request;
|
||||
|
||||
// step 1: send http request if the requestQueue isn't empty
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_requestQueueMutex);
|
||||
while (_requestQueue.empty()) {
|
||||
_sleepCondition.wait(_requestQueueMutex);
|
||||
}
|
||||
request = _requestQueue.at(0);
|
||||
_requestQueue.erase(0);
|
||||
}
|
||||
|
||||
if (request == _requestSentinel) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
HttpResponse *response = ccnew HttpResponse(request);
|
||||
response->addRef();
|
||||
|
||||
processResponse(response, _responseMessage);
|
||||
|
||||
// add response packet into queue
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.pushBack(response);
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread(CC_CALLBACK_0(HttpClient::dispatchResponseCallbacks, this));
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
}
|
||||
|
||||
// cleanup: if worker thread received quit signal, clean up un-completed request queue
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.clear();
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.clear();
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThreadAlone(HttpRequest *request, HttpResponse *response) {
|
||||
increaseThreadCount();
|
||||
|
||||
char responseMessage[RESPONSE_BUFFER_SIZE] = {0};
|
||||
processResponse(response, responseMessage);
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread([this, response, request] {
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
});
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
//Process Request
|
||||
static int processTask(HttpClient *client, HttpRequest *request, NSString *requestType, void *stream, long *responseCode, void *headerStream, char *errorBuffer) {
|
||||
if (nullptr == client) {
|
||||
strcpy(errorBuffer, "client object is invalid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//create request with url
|
||||
NSString *urlstring = [NSString stringWithUTF8String:request->getUrl()];
|
||||
NSURL *url = [NSURL URLWithString:urlstring];
|
||||
|
||||
NSMutableURLRequest *nsrequest = [NSMutableURLRequest requestWithURL:url
|
||||
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
|
||||
timeoutInterval:request->getTimeout()];
|
||||
|
||||
//set request type
|
||||
[nsrequest setHTTPMethod:requestType];
|
||||
|
||||
/* get custom header data (if set) */
|
||||
ccstd::vector<ccstd::string> headers = request->getHeaders();
|
||||
if (!headers.empty()) {
|
||||
/* append custom headers one by one */
|
||||
for (auto &header : headers) {
|
||||
unsigned long i = header.find(':', 0);
|
||||
unsigned long length = header.size();
|
||||
ccstd::string field = header.substr(0, i);
|
||||
ccstd::string value = header.substr(i + 1);
|
||||
// trim \n at the end of the string
|
||||
if (!value.empty() && value[value.size() - 1] == '\n') {
|
||||
value.erase(value.size() - 1);
|
||||
}
|
||||
// trim leading space (header is field: value format)
|
||||
if (!value.empty() && value[0] == ' ') {
|
||||
value.erase(0, 1);
|
||||
}
|
||||
NSString *headerField = [NSString stringWithUTF8String:field.c_str()];
|
||||
NSString *headerValue = [NSString stringWithUTF8String:value.c_str()];
|
||||
[nsrequest setValue:headerValue forHTTPHeaderField:headerField];
|
||||
}
|
||||
}
|
||||
|
||||
//if request type is post or put,set header and data
|
||||
if ([requestType isEqual:@"POST"] || [requestType isEqual:@"PUT"] || [requestType isEqual:@"PATCH"]) {
|
||||
char *requestDataBuffer = request->getRequestData();
|
||||
if (nullptr != requestDataBuffer && 0 != request->getRequestDataSize()) {
|
||||
NSData *postData = [NSData dataWithBytes:requestDataBuffer length:request->getRequestDataSize()];
|
||||
[nsrequest setHTTPBody:postData];
|
||||
}
|
||||
}
|
||||
|
||||
//read cookie properties from file and set cookie
|
||||
ccstd::string cookieFilename = client->getCookieFilename();
|
||||
if (!cookieFilename.empty() && nullptr != client->getCookie()) {
|
||||
const CookiesInfo *cookieInfo = client->getCookie()->getMatchCookie(request->getUrl());
|
||||
if (cookieInfo != nullptr) {
|
||||
NSString *domain = [NSString stringWithCString:cookieInfo->domain.c_str() encoding:[NSString defaultCStringEncoding]];
|
||||
NSString *path = [NSString stringWithCString:cookieInfo->path.c_str() encoding:[NSString defaultCStringEncoding]];
|
||||
NSString *value = [NSString stringWithCString:cookieInfo->value.c_str() encoding:[NSString defaultCStringEncoding]];
|
||||
NSString *name = [NSString stringWithCString:cookieInfo->name.c_str() encoding:[NSString defaultCStringEncoding]];
|
||||
|
||||
// create the properties for a cookie
|
||||
NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys:name, NSHTTPCookieName,
|
||||
value, NSHTTPCookieValue, path, NSHTTPCookiePath,
|
||||
domain, NSHTTPCookieDomain,
|
||||
nil];
|
||||
|
||||
// create the cookie from the properties
|
||||
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties];
|
||||
|
||||
// add the cookie to the cookie storage
|
||||
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
|
||||
}
|
||||
}
|
||||
|
||||
HttpAsynConnection *httpAsynConn = [[HttpAsynConnection new] autorelease];
|
||||
httpAsynConn.srcURL = urlstring;
|
||||
httpAsynConn.sslFile = nil;
|
||||
|
||||
ccstd::string sslCaFileName = client->getSSLVerification();
|
||||
if (!sslCaFileName.empty()) {
|
||||
long len = sslCaFileName.length();
|
||||
long pos = sslCaFileName.rfind('.', len - 1);
|
||||
|
||||
httpAsynConn.sslFile = [NSString stringWithUTF8String:sslCaFileName.substr(0, pos).c_str()];
|
||||
}
|
||||
[httpAsynConn startRequest:nsrequest];
|
||||
|
||||
while (httpAsynConn.finish != true) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
||||
}
|
||||
|
||||
//if http connection return error
|
||||
if (httpAsynConn.connError != nil) {
|
||||
NSString *errorString = [httpAsynConn.connError localizedDescription];
|
||||
strcpy(errorBuffer, [errorString UTF8String]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
//if http response got error, just log the error
|
||||
if (httpAsynConn.responseError != nil) {
|
||||
NSString *errorString = [httpAsynConn.responseError localizedDescription];
|
||||
strcpy(errorBuffer, [errorString UTF8String]);
|
||||
}
|
||||
|
||||
*responseCode = httpAsynConn.responseCode;
|
||||
|
||||
//add cookie to cookies vector
|
||||
if (!cookieFilename.empty()) {
|
||||
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:httpAsynConn.responseHeader forURL:url];
|
||||
for (NSHTTPCookie *cookie in cookies) {
|
||||
//NSLog(@"Cookie: %@", cookie);
|
||||
NSString *domain = cookie.domain;
|
||||
//BOOL session = cookie.sessionOnly;
|
||||
NSString *path = cookie.path;
|
||||
BOOL secure = cookie.isSecure;
|
||||
NSDate *date = cookie.expiresDate;
|
||||
NSString *name = cookie.name;
|
||||
NSString *value = cookie.value;
|
||||
|
||||
CookiesInfo cookieInfo;
|
||||
cookieInfo.domain = [domain cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
cookieInfo.path = [path cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
cookieInfo.secure = (secure == YES) ? true : false;
|
||||
cookieInfo.expires = [[NSString stringWithFormat:@"%ld", (long)[date timeIntervalSince1970]] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
cookieInfo.name = [name cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
cookieInfo.value = [value cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
cookieInfo.tailmatch = true;
|
||||
|
||||
client->getCookie()->updateOrAddCookie(&cookieInfo);
|
||||
}
|
||||
}
|
||||
|
||||
//handle response header
|
||||
NSMutableString *header = [NSMutableString string];
|
||||
[header appendFormat:@"HTTP/1.1 %ld %@\n", (long)httpAsynConn.responseCode, httpAsynConn.statusString];
|
||||
for (id key in httpAsynConn.responseHeader) {
|
||||
[header appendFormat:@"%@: %@\n", key, [httpAsynConn.responseHeader objectForKey:key]];
|
||||
}
|
||||
if (header.length > 0) {
|
||||
NSRange range = NSMakeRange(header.length - 1, 1);
|
||||
[header deleteCharactersInRange:range];
|
||||
}
|
||||
NSData *headerData = [header dataUsingEncoding:NSUTF8StringEncoding];
|
||||
ccstd::vector<char> *headerBuffer = (ccstd::vector<char> *)headerStream;
|
||||
const void *headerptr = [headerData bytes];
|
||||
long headerlen = [headerData length];
|
||||
headerBuffer->insert(headerBuffer->end(), (char *)headerptr, (char *)headerptr + headerlen);
|
||||
|
||||
//handle response data
|
||||
ccstd::vector<char> *recvBuffer = (ccstd::vector<char> *)stream;
|
||||
const void *ptr = [httpAsynConn.responseData bytes];
|
||||
long len = [httpAsynConn.responseData length];
|
||||
recvBuffer->insert(recvBuffer->end(), (char *)ptr, (char *)ptr + len);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// HttpClient implementation
|
||||
HttpClient *HttpClient::getInstance() {
|
||||
if (_httpClient == nullptr) {
|
||||
_httpClient = ccnew HttpClient();
|
||||
}
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
void HttpClient::destroyInstance() {
|
||||
if (nullptr == _httpClient) {
|
||||
CC_LOG_DEBUG("HttpClient singleton is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance begin");
|
||||
|
||||
auto thiz = _httpClient;
|
||||
_httpClient = nullptr;
|
||||
|
||||
if (auto sche = thiz->_scheduler.lock()) {
|
||||
sche->unscheduleAllForTarget(thiz);
|
||||
}
|
||||
thiz->_schedulerMutex.lock();
|
||||
thiz->_scheduler.reset();
|
||||
thiz->_schedulerMutex.unlock();
|
||||
|
||||
thiz->_requestQueueMutex.lock();
|
||||
thiz->_requestQueue.pushBack(thiz->_requestSentinel);
|
||||
thiz->_requestQueueMutex.unlock();
|
||||
|
||||
thiz->_sleepCondition.notify_one();
|
||||
thiz->decreaseThreadCountAndMayDeleteThis();
|
||||
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance() finished!");
|
||||
}
|
||||
|
||||
void HttpClient::enableCookies(const char *cookieFile) {
|
||||
_cookieFileMutex.lock();
|
||||
if (cookieFile) {
|
||||
_cookieFilename = ccstd::string(cookieFile);
|
||||
_cookieFilename = FileUtils::getInstance()->fullPathForFilename(_cookieFilename);
|
||||
} else {
|
||||
_cookieFilename = (FileUtils::getInstance()->getWritablePath() + "cookieFile.txt");
|
||||
}
|
||||
_cookieFileMutex.unlock();
|
||||
|
||||
if (nullptr == _cookie) {
|
||||
_cookie = ccnew HttpCookie;
|
||||
}
|
||||
_cookie->setCookieFileName(_cookieFilename);
|
||||
_cookie->readFile();
|
||||
}
|
||||
|
||||
void HttpClient::setSSLVerification(const ccstd::string &caFile) {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
_sslCaFilename = caFile;
|
||||
}
|
||||
|
||||
HttpClient::HttpClient()
|
||||
: _isInited(false),
|
||||
_timeoutForConnect(30),
|
||||
_timeoutForRead(60),
|
||||
_threadCount(0),
|
||||
_cookie(nullptr) {
|
||||
CC_LOG_DEBUG("In the constructor of HttpClient!");
|
||||
if (gThreadPool == nullptr) {
|
||||
gThreadPool = LegacyThreadPool::newFixedThreadPool(4);
|
||||
}
|
||||
_requestSentinel = ccnew HttpRequest();
|
||||
_requestSentinel->addRef();
|
||||
memset(_responseMessage, 0, sizeof(char) * RESPONSE_BUFFER_SIZE);
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
increaseThreadCount();
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient() {
|
||||
CC_SAFE_RELEASE(_requestSentinel);
|
||||
if (!_cookieFilename.empty() && nullptr != _cookie) {
|
||||
_cookie->writeFile();
|
||||
CC_SAFE_DELETE(_cookie);
|
||||
}
|
||||
CC_LOG_DEBUG("HttpClient destructor");
|
||||
}
|
||||
|
||||
//Lazy create semaphore & mutex & thread
|
||||
bool HttpClient::lazyInitThreadSemaphore() {
|
||||
if (_isInited) {
|
||||
return true;
|
||||
} else {
|
||||
auto t = std::thread(CC_CALLBACK_0(HttpClient::networkThread, this));
|
||||
t.detach();
|
||||
_isInited = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//Add a get task to queue
|
||||
void HttpClient::send(HttpRequest *request) {
|
||||
if (false == lazyInitThreadSemaphore()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.pushBack(request);
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
// Notify thread start to work
|
||||
_sleepCondition.notify_one();
|
||||
}
|
||||
|
||||
void HttpClient::sendImmediate(HttpRequest *request) {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
HttpResponse *response = ccnew HttpResponse(request);
|
||||
response->addRef(); // NOTE: RefCounted object's reference count is changed to 0 now. so needs to addRef after ccnew.
|
||||
|
||||
gThreadPool->pushTask([this, request, response](int /*tid*/) { HttpClient::networkThreadAlone(request, response); });
|
||||
}
|
||||
|
||||
// Poll and notify main thread if responses exists in queue
|
||||
void HttpClient::dispatchResponseCallbacks() {
|
||||
// log("CCHttpClient::dispatchResponseCallbacks is running");
|
||||
//occurs when cocos thread fires but the network thread has already quited
|
||||
HttpResponse *response = nullptr;
|
||||
_responseQueueMutex.lock();
|
||||
if (!_responseQueue.empty()) {
|
||||
response = _responseQueue.at(0);
|
||||
_responseQueue.erase(0);
|
||||
}
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
if (response) {
|
||||
HttpRequest *request = response->getHttpRequest();
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
}
|
||||
}
|
||||
|
||||
// Process Response
|
||||
void HttpClient::processResponse(HttpResponse *response, char *responseMessage) {
|
||||
auto request = response->getHttpRequest();
|
||||
long responseCode = -1;
|
||||
int retValue = 0;
|
||||
NSString *requestType = nil;
|
||||
|
||||
// Process the request -> get response packet
|
||||
switch (request->getRequestType()) {
|
||||
case HttpRequest::Type::GET: // HTTP GET
|
||||
requestType = @"GET";
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::POST: // HTTP POST
|
||||
requestType = @"POST";
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PUT:
|
||||
requestType = @"PUT";
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::HEAD:
|
||||
requestType = @"HEAD";
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::DELETE:
|
||||
requestType = @"DELETE";
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PATCH:
|
||||
requestType = @"PATCH";
|
||||
break;
|
||||
|
||||
default:
|
||||
CC_ABORT();
|
||||
break;
|
||||
}
|
||||
|
||||
retValue = processTask(this,
|
||||
request,
|
||||
requestType,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
|
||||
// write data to HttpResponse
|
||||
response->setResponseCode(responseCode);
|
||||
|
||||
if (retValue != 0) {
|
||||
response->setSucceed(true);
|
||||
} else {
|
||||
response->setSucceed(false);
|
||||
response->setErrorBuffer(responseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::increaseThreadCount() {
|
||||
_threadCountMutex.lock();
|
||||
++_threadCount;
|
||||
_threadCountMutex.unlock();
|
||||
}
|
||||
|
||||
void HttpClient::decreaseThreadCountAndMayDeleteThis() {
|
||||
bool needDeleteThis = false;
|
||||
_threadCountMutex.lock();
|
||||
--_threadCount;
|
||||
if (0 == _threadCount) {
|
||||
needDeleteThis = true;
|
||||
}
|
||||
|
||||
_threadCountMutex.unlock();
|
||||
if (needDeleteThis) {
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getCookieFilename() {
|
||||
std::lock_guard<std::mutex> lock(_cookieFileMutex);
|
||||
return _cookieFilename;
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getSSLVerification() {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
return _sslCaFilename;
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
969
cocos/network/HttpClient-java.cpp
Normal file
969
cocos/network/HttpClient-java.cpp
Normal file
@@ -0,0 +1,969 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2012 greathqy
|
||||
Copyright (c) 2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/HttpClient.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Log.h"
|
||||
#include "base/ThreadPool.h"
|
||||
#include "base/UTF8.h"
|
||||
#include "base/std/container/queue.h"
|
||||
#include "platform/FileUtils.h"
|
||||
#include "platform/java/jni/JniHelper.h"
|
||||
|
||||
#ifndef JCLS_HTTPCLIENT
|
||||
#define JCLS_HTTPCLIENT "com/cocos/lib/CocosHttpURLConnection"
|
||||
#endif
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
using HttpRequestHeaders = ccstd::vector<ccstd::string>;
|
||||
using HttpRequestHeadersIter = HttpRequestHeaders::iterator;
|
||||
using HttpCookies = ccstd::vector<ccstd::string>;
|
||||
using HttpCookiesIter = HttpCookies::iterator;
|
||||
|
||||
static HttpClient *gHttpClient = nullptr; // pointer to singleton
|
||||
static LegacyThreadPool *gThreadPool = nullptr;
|
||||
|
||||
struct CookiesInfo {
|
||||
ccstd::string domain;
|
||||
bool tailmatch;
|
||||
ccstd::string path;
|
||||
bool secure;
|
||||
ccstd::string key;
|
||||
ccstd::string value;
|
||||
ccstd::string expires;
|
||||
};
|
||||
|
||||
//static size_t writeData(void *ptr, size_t size, size_t nmemb, void *stream)
|
||||
static size_t writeData(void *buffer, size_t sizes, HttpResponse *response) {
|
||||
auto *recvBuffer = static_cast<ccstd::vector<char> *>(response->getResponseData());
|
||||
recvBuffer->clear();
|
||||
recvBuffer->insert(recvBuffer->end(), static_cast<char *>(buffer), (static_cast<char *>(buffer)) + sizes);
|
||||
return sizes;
|
||||
}
|
||||
|
||||
//static size_t writeHeaderData(void *ptr, size_t size, size_t nmemb, void *stream)
|
||||
size_t writeHeaderData(void *buffer, size_t sizes, HttpResponse *response) {
|
||||
auto *recvBuffer = static_cast<ccstd::vector<char> *>(response->getResponseHeader());
|
||||
recvBuffer->clear();
|
||||
recvBuffer->insert(recvBuffer->end(), static_cast<char *>(buffer), static_cast<char *>(buffer) + sizes);
|
||||
return sizes;
|
||||
}
|
||||
|
||||
class HttpURLConnection {
|
||||
public:
|
||||
explicit HttpURLConnection(HttpClient *httpClient)
|
||||
: _client(httpClient),
|
||||
_httpURLConnection(nullptr),
|
||||
_contentLength(0) {
|
||||
}
|
||||
|
||||
~HttpURLConnection() {
|
||||
if (_httpURLConnection != nullptr) {
|
||||
JniHelper::getEnv()->DeleteGlobalRef(_httpURLConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void setRequestMethod(const char *method) { // NOLINT
|
||||
_requestmethod = method;
|
||||
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"setRequestMethod",
|
||||
"(Ljava/net/HttpURLConnection;Ljava/lang/String;)V")) {
|
||||
jstring jstr = methodInfo.env->NewStringUTF(_requestmethod.c_str());
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, jstr);
|
||||
ccDeleteLocalRef(methodInfo.env, jstr);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
bool init(HttpRequest *request) {
|
||||
createHttpURLConnection(request->getUrl());
|
||||
if (!configure(request)) {
|
||||
return false;
|
||||
}
|
||||
/* get custom header data (if set) */
|
||||
HttpRequestHeaders headers = request->getHeaders();
|
||||
if (!headers.empty()) {
|
||||
/* append custom headers one by one */
|
||||
for (auto &header : headers) {
|
||||
uint32_t len = header.length();
|
||||
ccstd::string::size_type pos = header.find(':');
|
||||
if (ccstd::string::npos == pos || pos >= len) {
|
||||
continue;
|
||||
}
|
||||
ccstd::string str1 = header.substr(0, pos);
|
||||
ccstd::string str2 = header.substr(pos + 1);
|
||||
// trim \n at the end of the string
|
||||
if (!str2.empty() && str2[str2.size() - 1] == '\n') {
|
||||
str2.erase(str2.size() - 1);
|
||||
}
|
||||
// trim leading space (header is field: value format)
|
||||
if (!str2.empty() && str2[0] == ' ') {
|
||||
str2.erase(0, 1);
|
||||
}
|
||||
addRequestHeader(str1.c_str(), str2.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
addCookiesForRequestHeader();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int connect() { // NOLINT
|
||||
int suc = 0;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"connect",
|
||||
"(Ljava/net/HttpURLConnection;)I")) {
|
||||
suc = methodInfo.env->CallStaticIntMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return suc;
|
||||
}
|
||||
|
||||
void disconnect() { // NOLINT
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"disconnect",
|
||||
"(Ljava/net/HttpURLConnection;)V")) {
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
int getResponseCode() { // NOLINT
|
||||
int responseCode = 0;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseCode",
|
||||
"(Ljava/net/HttpURLConnection;)I")) {
|
||||
responseCode = methodInfo.env->CallStaticIntMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
char *getResponseMessage() { // NOLINT
|
||||
char *message = nullptr;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseMessage",
|
||||
"(Ljava/net/HttpURLConnection;)Ljava/lang/String;")) {
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
message = getBufferFromJString(static_cast<jstring>(jObj), methodInfo.env);
|
||||
if (nullptr != jObj) {
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
void sendRequest(HttpRequest *request) { // NOLINT
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"sendRequest",
|
||||
"(Ljava/net/HttpURLConnection;[B)V")) {
|
||||
jbyteArray bytearray;
|
||||
auto dataSize = static_cast<ssize_t>(request->getRequestDataSize());
|
||||
bytearray = methodInfo.env->NewByteArray(dataSize);
|
||||
methodInfo.env->SetByteArrayRegion(bytearray, 0, dataSize, reinterpret_cast<const jbyte *>(request->getRequestData()));
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, bytearray);
|
||||
ccDeleteLocalRef(methodInfo.env, bytearray);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
size_t saveResponseCookies(const char *responseCookies, size_t count) { // NOLINT
|
||||
if (nullptr == responseCookies || strlen(responseCookies) == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_cookieFileName.empty()) {
|
||||
_cookieFileName = FileUtils::getInstance()->getWritablePath() + "cookieFile.txt";
|
||||
}
|
||||
|
||||
FILE *fp = fopen(_cookieFileName.c_str(), "w");
|
||||
if (nullptr == fp) {
|
||||
CC_LOG_DEBUG("can't create or open response cookie files");
|
||||
return 0;
|
||||
}
|
||||
|
||||
fwrite(responseCookies, sizeof(char), count, fp);
|
||||
|
||||
fclose(fp);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
char *getResponseHeaders() { // NOLINT
|
||||
char *headers = nullptr;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseHeaders",
|
||||
"(Ljava/net/HttpURLConnection;)Ljava/lang/String;")) {
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
headers = getBufferFromJString(static_cast<jstring>(jObj), methodInfo.env);
|
||||
if (nullptr != jObj) {
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
char *getResponseContent(HttpResponse *response) { // NOLINT
|
||||
if (nullptr == response) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
char *content = nullptr;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseContent",
|
||||
"(Ljava/net/HttpURLConnection;)[B")) {
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection);
|
||||
|
||||
_contentLength = getCStrFromJByteArray(static_cast<jbyteArray>(jObj), methodInfo.env, &content);
|
||||
if (nullptr != jObj) {
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
char *getResponseHeaderByKey(const char *key) {
|
||||
char *value = nullptr;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseHeaderByKey",
|
||||
"(Ljava/net/HttpURLConnection;Ljava/lang/String;)Ljava/lang/String;")) {
|
||||
jstring jstrKey = methodInfo.env->NewStringUTF(key);
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, jstrKey);
|
||||
value = getBufferFromJString(static_cast<jstring>(jObj), methodInfo.env);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrKey);
|
||||
if (nullptr != jObj) {
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
int getResponseHeaderByKeyInt(const char *key) {
|
||||
int contentLength = 0;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseHeaderByKeyInt",
|
||||
"(Ljava/net/HttpURLConnection;Ljava/lang/String;)I")) {
|
||||
jstring jstrKey = methodInfo.env->NewStringUTF(key);
|
||||
contentLength = methodInfo.env->CallStaticIntMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, jstrKey);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrKey);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
char *getResponseHeaderByIdx(int idx) {
|
||||
char *header = nullptr;
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"getResponseHeaderByIdx",
|
||||
"(Ljava/net/HttpURLConnection;I)Ljava/lang/String;")) {
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, idx);
|
||||
header = getBufferFromJString(static_cast<jstring>(jObj), methodInfo.env);
|
||||
if (nullptr != jObj) {
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
}
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
const ccstd::string &getCookieFileName() const {
|
||||
return _cookieFileName;
|
||||
}
|
||||
|
||||
void setCookieFileName(ccstd::string &filename) { //NOLINT
|
||||
_cookieFileName = filename;
|
||||
}
|
||||
|
||||
int getContentLength() const {
|
||||
return _contentLength;
|
||||
}
|
||||
|
||||
private:
|
||||
void createHttpURLConnection(const ccstd::string &url) {
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"createHttpURLConnection",
|
||||
"(Ljava/lang/String;)Ljava/net/HttpURLConnection;")) {
|
||||
_url = url;
|
||||
jstring jurl = methodInfo.env->NewStringUTF(url.c_str());
|
||||
jobject jObj = methodInfo.env->CallStaticObjectMethod(methodInfo.classID, methodInfo.methodID, jurl);
|
||||
_httpURLConnection = methodInfo.env->NewGlobalRef(jObj);
|
||||
ccDeleteLocalRef(methodInfo.env, jurl);
|
||||
ccDeleteLocalRef(methodInfo.env, jObj);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
void addRequestHeader(const char *key, const char *value) {
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"addRequestHeader",
|
||||
"(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;)V")) {
|
||||
jstring jstrKey = methodInfo.env->NewStringUTF(key);
|
||||
jstring jstrVal = methodInfo.env->NewStringUTF(value);
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, jstrKey, jstrVal);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrKey);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrVal);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
void addCookiesForRequestHeader() {
|
||||
if (_client->getCookieFilename().empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_cookieFileName = FileUtils::getInstance()->fullPathForFilename(_client->getCookieFilename());
|
||||
|
||||
ccstd::string cookiesInfo = FileUtils::getInstance()->getStringFromFile(_cookieFileName);
|
||||
|
||||
if (cookiesInfo.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HttpCookies cookiesVec;
|
||||
cookiesVec.clear();
|
||||
|
||||
std::stringstream stream(cookiesInfo);
|
||||
ccstd::string item;
|
||||
while (std::getline(stream, item, '\n')) {
|
||||
cookiesVec.push_back(item);
|
||||
}
|
||||
|
||||
if (cookiesVec.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ccstd::vector<CookiesInfo> cookiesInfoVec;
|
||||
cookiesInfoVec.clear();
|
||||
|
||||
for (auto &cookies : cookiesVec) {
|
||||
if (cookies.find("#HttpOnly_") != ccstd::string::npos) {
|
||||
cookies = cookies.substr(10);
|
||||
}
|
||||
|
||||
if (cookies.at(0) == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
CookiesInfo co;
|
||||
std::stringstream streamInfo(cookies);
|
||||
ccstd::string item;
|
||||
ccstd::vector<ccstd::string> elems;
|
||||
|
||||
while (std::getline(streamInfo, item, '\t')) {
|
||||
elems.push_back(item);
|
||||
}
|
||||
|
||||
co.domain = elems[0];
|
||||
if (co.domain.at(0) == '.') {
|
||||
co.domain = co.domain.substr(1);
|
||||
}
|
||||
co.tailmatch = strcmp("TRUE", elems.at(1).c_str()) != 0;
|
||||
co.path = elems.at(2);
|
||||
co.secure = strcmp("TRUE", elems.at(3).c_str()) != 0;
|
||||
co.expires = elems.at(4);
|
||||
co.key = elems.at(5);
|
||||
co.value = elems.at(6);
|
||||
cookiesInfoVec.push_back(co);
|
||||
}
|
||||
|
||||
ccstd::string sendCookiesInfo;
|
||||
int cookiesCount = 0;
|
||||
for (auto &cookieInfo : cookiesInfoVec) {
|
||||
if (_url.find(cookieInfo.domain) != ccstd::string::npos) {
|
||||
ccstd::string keyValue = cookieInfo.key;
|
||||
keyValue.append("=");
|
||||
keyValue.append(cookieInfo.value);
|
||||
if (cookiesCount != 0) {
|
||||
sendCookiesInfo.append(";");
|
||||
}
|
||||
|
||||
sendCookiesInfo.append(keyValue);
|
||||
}
|
||||
cookiesCount++;
|
||||
}
|
||||
|
||||
//set Cookie
|
||||
addRequestHeader("Cookie", sendCookiesInfo.c_str());
|
||||
}
|
||||
|
||||
void setReadAndConnectTimeout(int readMiliseconds, int connectMiliseconds) {
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"setReadAndConnectTimeout",
|
||||
"(Ljava/net/HttpURLConnection;II)V")) {
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, readMiliseconds, connectMiliseconds);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
void setVerifySSL() {
|
||||
if (_client->getSSLVerification().empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ccstd::string fullpath = FileUtils::getInstance()->fullPathForFilename(_client->getSSLVerification());
|
||||
|
||||
JniMethodInfo methodInfo;
|
||||
if (JniHelper::getStaticMethodInfo(methodInfo,
|
||||
JCLS_HTTPCLIENT,
|
||||
"setVerifySSL",
|
||||
"(Ljava/net/HttpURLConnection;Ljava/lang/String;)V")) {
|
||||
jstring jstrfullpath = methodInfo.env->NewStringUTF(fullpath.c_str());
|
||||
methodInfo.env->CallStaticVoidMethod(
|
||||
methodInfo.classID, methodInfo.methodID, _httpURLConnection, jstrfullpath);
|
||||
ccDeleteLocalRef(methodInfo.env, jstrfullpath);
|
||||
ccDeleteLocalRef(methodInfo.env, methodInfo.classID);
|
||||
} else {
|
||||
CC_LOG_ERROR("HttpClient::%s failed!", __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
bool configure(HttpRequest *request) {
|
||||
if (nullptr == _httpURLConnection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nullptr == _client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setReadAndConnectTimeout(static_cast<int>(request->getTimeout() * 1000),
|
||||
static_cast<int>(request->getTimeout() * 1000));
|
||||
|
||||
setVerifySSL();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
char *getBufferFromJString(jstring jstr, JNIEnv *env) { //NOLINT(readability-convert-member-functions-to-static)
|
||||
if (nullptr == jstr) {
|
||||
return nullptr;
|
||||
}
|
||||
ccstd::string strValue = cc::StringUtils::getStringUTFCharsJNI(env, jstr);
|
||||
size_t size = strValue.size() + 1;
|
||||
char *retVal = static_cast<char *>(malloc(size));
|
||||
if (retVal == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
memcpy(retVal, strValue.c_str(), size);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
int getCStrFromJByteArray(jbyteArray jba, JNIEnv *env, char **ppData) { //NOLINT(readability-convert-member-functions-to-static)
|
||||
if (nullptr == jba) {
|
||||
*ppData = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int len = env->GetArrayLength(jba);
|
||||
auto *str = static_cast<jbyte *>(malloc(sizeof(jbyte) * len));
|
||||
env->GetByteArrayRegion(jba, 0, len, str);
|
||||
|
||||
*ppData = reinterpret_cast<char *>(str);
|
||||
return len;
|
||||
}
|
||||
|
||||
const ccstd::string &getCookieString() const {
|
||||
return _responseCookies;
|
||||
}
|
||||
|
||||
private: // NOLINT(readability-redundant-access-specifiers)
|
||||
HttpClient *_client;
|
||||
jobject _httpURLConnection;
|
||||
ccstd::string _requestmethod;
|
||||
ccstd::string _responseCookies;
|
||||
ccstd::string _cookieFileName;
|
||||
ccstd::string _url;
|
||||
int _contentLength;
|
||||
};
|
||||
|
||||
// Process Response
|
||||
void HttpClient::processResponse(HttpResponse *response, char *responseMessage) {
|
||||
auto *request = response->getHttpRequest();
|
||||
HttpRequest::Type requestType = request->getRequestType();
|
||||
|
||||
if (HttpRequest::Type::GET != requestType &&
|
||||
HttpRequest::Type::POST != requestType &&
|
||||
HttpRequest::Type::PUT != requestType &&
|
||||
HttpRequest::Type::HEAD != requestType &&
|
||||
HttpRequest::Type::DELETE != requestType) {
|
||||
CC_ABORT();
|
||||
return;
|
||||
}
|
||||
|
||||
long responseCode = -1L; // NOLINT
|
||||
int retValue = 0;
|
||||
|
||||
HttpURLConnection urlConnection(this);
|
||||
if (!urlConnection.init(request)) {
|
||||
response->setSucceed(false);
|
||||
response->setErrorBuffer("HttpURLConnection init failed");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestType) {
|
||||
case HttpRequest::Type::GET:
|
||||
urlConnection.setRequestMethod("GET");
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::POST:
|
||||
urlConnection.setRequestMethod("POST");
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PUT:
|
||||
urlConnection.setRequestMethod("PUT");
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::HEAD:
|
||||
urlConnection.setRequestMethod("HEAD");
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::DELETE:
|
||||
urlConnection.setRequestMethod("DELETE");
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PATCH:
|
||||
urlConnection.setRequestMethod("PATCH");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
int suc = urlConnection.connect();
|
||||
if (0 != suc) {
|
||||
response->setSucceed(false);
|
||||
response->setErrorBuffer("connect failed");
|
||||
response->setResponseCode(responseCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HttpRequest::Type::POST == requestType ||
|
||||
HttpRequest::Type::PUT == requestType ||
|
||||
HttpRequest::Type::PATCH == requestType) {
|
||||
urlConnection.sendRequest(request);
|
||||
}
|
||||
|
||||
responseCode = urlConnection.getResponseCode();
|
||||
|
||||
if (0 == responseCode) {
|
||||
response->setSucceed(false);
|
||||
response->setErrorBuffer("connect failed");
|
||||
response->setResponseCode(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
char *headers = urlConnection.getResponseHeaders();
|
||||
if (nullptr != headers) {
|
||||
writeHeaderData(headers, strlen(headers), response);
|
||||
}
|
||||
free(headers);
|
||||
|
||||
//get and save cookies
|
||||
char *cookiesInfo = urlConnection.getResponseHeaderByKey("set-cookie");
|
||||
if (nullptr != cookiesInfo) {
|
||||
urlConnection.saveResponseCookies(cookiesInfo, strlen(cookiesInfo));
|
||||
}
|
||||
free(cookiesInfo);
|
||||
|
||||
//content len
|
||||
int contentLength = urlConnection.getResponseHeaderByKeyInt("Content-Length");
|
||||
char *contentInfo = urlConnection.getResponseContent(response);
|
||||
if (nullptr != contentInfo) {
|
||||
auto *recvBuffer = static_cast<ccstd::vector<char> *>(response->getResponseData());
|
||||
recvBuffer->clear();
|
||||
recvBuffer->insert(recvBuffer->begin(), contentInfo, contentInfo + urlConnection.getContentLength());
|
||||
}
|
||||
free(contentInfo);
|
||||
|
||||
char *messageInfo = urlConnection.getResponseMessage();
|
||||
if (messageInfo) {
|
||||
strcpy(responseMessage, messageInfo);
|
||||
free(messageInfo);
|
||||
}
|
||||
|
||||
urlConnection.disconnect();
|
||||
|
||||
// write data to HttpResponse
|
||||
response->setResponseCode(responseCode);
|
||||
|
||||
if (responseCode == -1) {
|
||||
response->setSucceed(false);
|
||||
if (responseMessage != nullptr) {
|
||||
response->setErrorBuffer(responseMessage);
|
||||
} else {
|
||||
response->setErrorBuffer("response code error!");
|
||||
}
|
||||
} else {
|
||||
response->setSucceed(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThread() {
|
||||
increaseThreadCount();
|
||||
|
||||
while (true) {
|
||||
HttpRequest *request;
|
||||
|
||||
// step 1: send http request if the requestQueue isn't empty
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_requestQueueMutex);
|
||||
while (_requestQueue.empty()) {
|
||||
_sleepCondition.wait(_requestQueueMutex);
|
||||
}
|
||||
request = _requestQueue.at(0);
|
||||
_requestQueue.erase(0);
|
||||
}
|
||||
|
||||
if (request == _requestSentinel) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
auto *response = ccnew HttpResponse(request);
|
||||
response->addRef(); // NOTE: RefCounted object's reference count is changed to 0 now. so needs to addRef after ccnew.
|
||||
processResponse(response, _responseMessage);
|
||||
|
||||
// add response packet into queue
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.pushBack(response);
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread(CC_CALLBACK_0(HttpClient::dispatchResponseCallbacks, this)); // NOLINT
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
}
|
||||
|
||||
// cleanup: if worker thread received quit signal, clean up un-completed request queue
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.clear();
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.clear();
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThreadAlone(HttpRequest *request, HttpResponse *response) {
|
||||
increaseThreadCount();
|
||||
|
||||
char responseMessage[RESPONSE_BUFFER_SIZE] = {0};
|
||||
processResponse(response, responseMessage);
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread([this, response, request] {
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
});
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
// HttpClient implementation
|
||||
HttpClient *HttpClient::getInstance() {
|
||||
if (gHttpClient == nullptr) {
|
||||
gHttpClient = ccnew HttpClient();
|
||||
}
|
||||
|
||||
return gHttpClient;
|
||||
}
|
||||
|
||||
void HttpClient::destroyInstance() {
|
||||
if (gHttpClient == nullptr) {
|
||||
CC_LOG_DEBUG("HttpClient singleton is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance ...");
|
||||
|
||||
auto *thiz = gHttpClient;
|
||||
gHttpClient = nullptr;
|
||||
|
||||
if (auto sche = thiz->_scheduler.lock()) {
|
||||
sche->unscheduleAllForTarget(thiz);
|
||||
}
|
||||
thiz->_schedulerMutex.lock();
|
||||
thiz->_scheduler.reset();
|
||||
thiz->_schedulerMutex.unlock();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thiz->_requestQueueMutex);
|
||||
thiz->_requestQueue.pushBack(thiz->_requestSentinel);
|
||||
}
|
||||
thiz->_sleepCondition.notify_one();
|
||||
|
||||
thiz->decreaseThreadCountAndMayDeleteThis();
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance() finished!");
|
||||
}
|
||||
|
||||
void HttpClient::enableCookies(const char *cookieFile) {
|
||||
std::lock_guard<std::mutex> lock(_cookieFileMutex);
|
||||
if (cookieFile) {
|
||||
_cookieFilename = ccstd::string(cookieFile);
|
||||
} else {
|
||||
_cookieFilename = (FileUtils::getInstance()->getWritablePath() + "cookieFile.txt");
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::setSSLVerification(const ccstd::string &caFile) {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
_sslCaFilename = caFile;
|
||||
}
|
||||
|
||||
HttpClient::HttpClient()
|
||||
: _isInited(false),
|
||||
_timeoutForConnect(30),
|
||||
_timeoutForRead(60),
|
||||
_threadCount(0),
|
||||
_cookie(nullptr),
|
||||
_requestSentinel(ccnew HttpRequest()) {
|
||||
CC_LOG_DEBUG("In the constructor of HttpClient!");
|
||||
_requestSentinel->addRef();
|
||||
if (gThreadPool == nullptr) {
|
||||
gThreadPool = LegacyThreadPool::newFixedThreadPool(4);
|
||||
}
|
||||
increaseThreadCount();
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient() {
|
||||
CC_LOG_DEBUG("In the destructor of HttpClient!");
|
||||
CC_SAFE_RELEASE(_requestSentinel);
|
||||
}
|
||||
|
||||
//Lazy create semaphore & mutex & thread
|
||||
bool HttpClient::lazyInitThreadSemaphore() {
|
||||
if (_isInited) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto t = std::thread(CC_CALLBACK_0(HttpClient::networkThread, this)); // NOLINT
|
||||
t.detach();
|
||||
_isInited = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//Add a get task to queue
|
||||
void HttpClient::send(HttpRequest *request) {
|
||||
if (!lazyInitThreadSemaphore()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nullptr == request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.pushBack(request);
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
// Notify thread start to work
|
||||
_sleepCondition.notify_one();
|
||||
}
|
||||
|
||||
void HttpClient::sendImmediate(HttpRequest *request) {
|
||||
if (nullptr == request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
auto *response = ccnew HttpResponse(request);
|
||||
response->addRef(); // NOTE: RefCounted object's reference count is changed to 0 now. so needs to addRef after ccnew.
|
||||
|
||||
gThreadPool->pushTask([this, request, response](int /*tid*/) { HttpClient::networkThreadAlone(request, response); });
|
||||
}
|
||||
|
||||
// Poll and notify main thread if responses exists in queue
|
||||
void HttpClient::dispatchResponseCallbacks() {
|
||||
// log("CCHttpClient::dispatchResponseCallbacks is running");
|
||||
//occurs when cocos thread fires but the network thread has already quited
|
||||
HttpResponse *response = nullptr;
|
||||
|
||||
_responseQueueMutex.lock();
|
||||
|
||||
if (!_responseQueue.empty()) {
|
||||
response = _responseQueue.at(0);
|
||||
_responseQueue.erase(0);
|
||||
}
|
||||
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
if (response) {
|
||||
HttpRequest *request = response->getHttpRequest();
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::increaseThreadCount() {
|
||||
_threadCountMutex.lock();
|
||||
++_threadCount;
|
||||
_threadCountMutex.unlock();
|
||||
}
|
||||
|
||||
void HttpClient::decreaseThreadCountAndMayDeleteThis() {
|
||||
bool needDeleteThis = false;
|
||||
_threadCountMutex.lock();
|
||||
--_threadCount;
|
||||
if (0 == _threadCount) {
|
||||
needDeleteThis = true;
|
||||
}
|
||||
|
||||
_threadCountMutex.unlock();
|
||||
if (needDeleteThis) {
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getCookieFilename() {
|
||||
std::lock_guard<std::mutex> lock(_cookieFileMutex);
|
||||
return _cookieFilename;
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getSSLVerification() {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
return _sslCaFilename;
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
579
cocos/network/HttpClient.cpp
Normal file
579
cocos/network/HttpClient.cpp
Normal file
@@ -0,0 +1,579 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2012 greathqy
|
||||
Copyright (c) 2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/HttpClient.h"
|
||||
#include <curl/curl.h>
|
||||
#include <errno.h>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Log.h"
|
||||
#include "base/ThreadPool.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "platform/FileUtils.h"
|
||||
#include "platform/StdC.h"
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
#if (CC_PLATFORM == CC_PLATFORM_WINDOWS)
|
||||
typedef int int32_t;
|
||||
#endif
|
||||
|
||||
static HttpClient *_httpClient = nullptr; // pointer to singleton
|
||||
static LegacyThreadPool *gThreadPool = nullptr;
|
||||
|
||||
typedef size_t (*write_callback)(void *ptr, size_t size, size_t nmemb, void *stream);
|
||||
|
||||
// Callback function used by libcurl for collect response data
|
||||
static size_t writeData(void *ptr, size_t size, size_t nmemb, void *stream) {
|
||||
ccstd::vector<char> *recvBuffer = (ccstd::vector<char> *)stream;
|
||||
size_t sizes = size * nmemb;
|
||||
|
||||
// add data to the end of recvBuffer
|
||||
// write data maybe called more than once in a single request
|
||||
recvBuffer->insert(recvBuffer->end(), (char *)ptr, (char *)ptr + sizes);
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
// Callback function used by libcurl for collect header data
|
||||
static size_t writeHeaderData(void *ptr, size_t size, size_t nmemb, void *stream) {
|
||||
ccstd::vector<char> *recvBuffer = (ccstd::vector<char> *)stream;
|
||||
size_t sizes = size * nmemb;
|
||||
|
||||
// add data to the end of recvBuffer
|
||||
// write data maybe called more than once in a single request
|
||||
recvBuffer->insert(recvBuffer->end(), (char *)ptr, (char *)ptr + sizes);
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
static int processGetTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
|
||||
static int processPostTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
|
||||
static int processPutTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
|
||||
static int processDeleteTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
|
||||
static int processPatchTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
|
||||
// int processDownloadTask(HttpRequest *task, write_callback callback, void *stream, int32_t *errorCode);
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThread() {
|
||||
increaseThreadCount();
|
||||
|
||||
while (true) {
|
||||
HttpRequest *request;
|
||||
|
||||
// step 1: send http request if the requestQueue isn't empty
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_requestQueueMutex);
|
||||
while (_requestQueue.empty()) {
|
||||
_sleepCondition.wait(_requestQueueMutex);
|
||||
}
|
||||
request = _requestQueue.at(0);
|
||||
_requestQueue.erase(0);
|
||||
}
|
||||
|
||||
if (request == _requestSentinel) {
|
||||
break;
|
||||
}
|
||||
|
||||
// step 2: libcurl sync access
|
||||
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
HttpResponse *response = ccnew HttpResponse(request);
|
||||
response->addRef(); // NOTE: RefCounted object's reference count is changed to 0 now. so needs to addRef after ccnew.
|
||||
|
||||
processResponse(response, _responseMessage);
|
||||
|
||||
// add response packet into queue
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.pushBack(response);
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread(CC_CALLBACK_0(HttpClient::dispatchResponseCallbacks, this));
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
}
|
||||
|
||||
// cleanup: if worker thread received quit signal, clean up un-completed request queue
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.clear();
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
_responseQueueMutex.lock();
|
||||
_responseQueue.clear();
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
// Worker thread
|
||||
void HttpClient::networkThreadAlone(HttpRequest *request, HttpResponse *response) {
|
||||
increaseThreadCount();
|
||||
|
||||
char responseMessage[RESPONSE_BUFFER_SIZE] = {0};
|
||||
processResponse(response, responseMessage);
|
||||
|
||||
_schedulerMutex.lock();
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread([this, response, request] {
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
});
|
||||
}
|
||||
_schedulerMutex.unlock();
|
||||
|
||||
decreaseThreadCountAndMayDeleteThis();
|
||||
}
|
||||
|
||||
//Configure curl's timeout property
|
||||
static bool configureCURL(HttpClient *client, HttpRequest *request, CURL *handle, char *errorBuffer) {
|
||||
if (!handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t code;
|
||||
code = curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, errorBuffer);
|
||||
if (code != CURLE_OK) {
|
||||
return false;
|
||||
}
|
||||
// In the openharmony platform, the long type must be used, otherwise there will be an exception.
|
||||
long timeout = static_cast<long>(request->getTimeout());
|
||||
code = curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout);
|
||||
if (code != CURLE_OK) {
|
||||
return false;
|
||||
}
|
||||
code = curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, timeout);
|
||||
if (code != CURLE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ccstd::string sslCaFilename = client->getSSLVerification();
|
||||
if (sslCaFilename.empty()) {
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
} else {
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(handle, CURLOPT_CAINFO, sslCaFilename.c_str());
|
||||
}
|
||||
|
||||
// FIXED #3224: The subthread of CCHttpClient interrupts main thread if timeout comes.
|
||||
// Document is here: http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTNOSIGNAL
|
||||
curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
|
||||
|
||||
curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class CURLRaii {
|
||||
/// Instance of CURL
|
||||
CURL *_curl;
|
||||
/// Keeps custom header data
|
||||
curl_slist *_headers;
|
||||
|
||||
public:
|
||||
CURLRaii()
|
||||
: _curl(curl_easy_init()),
|
||||
_headers(nullptr) {
|
||||
}
|
||||
|
||||
~CURLRaii() {
|
||||
if (_curl)
|
||||
curl_easy_cleanup(_curl);
|
||||
/* free the linked list for header data */
|
||||
if (_headers)
|
||||
curl_slist_free_all(_headers);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool setOption(CURLoption option, T data) {
|
||||
return CURLE_OK == curl_easy_setopt(_curl, option, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inits CURL instance for common usage
|
||||
* @param request Null not allowed
|
||||
* @param callback Response write callback
|
||||
* @param stream Response write stream
|
||||
*/
|
||||
bool init(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
if (!_curl)
|
||||
return false;
|
||||
if (!configureCURL(client, request, _curl, errorBuffer))
|
||||
return false;
|
||||
|
||||
/* get custom header data (if set) */
|
||||
ccstd::vector<ccstd::string> headers = request->getHeaders();
|
||||
if (!headers.empty()) {
|
||||
/* append custom headers one by one */
|
||||
for (auto &header : headers)
|
||||
_headers = curl_slist_append(_headers, header.c_str());
|
||||
/* set custom headers for curl */
|
||||
if (!setOption(CURLOPT_HTTPHEADER, _headers))
|
||||
return false;
|
||||
}
|
||||
ccstd::string cookieFilename = client->getCookieFilename();
|
||||
if (!cookieFilename.empty()) {
|
||||
if (!setOption(CURLOPT_COOKIEFILE, cookieFilename.c_str())) {
|
||||
return false;
|
||||
}
|
||||
if (!setOption(CURLOPT_COOKIEJAR, cookieFilename.c_str())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return setOption(CURLOPT_URL, request->getUrl()) && setOption(CURLOPT_WRITEFUNCTION, callback) && setOption(CURLOPT_WRITEDATA, stream) && setOption(CURLOPT_HEADERFUNCTION, headerCallback) && setOption(CURLOPT_HEADERDATA, headerStream);
|
||||
}
|
||||
|
||||
/// @param responseCode Null not allowed
|
||||
bool perform(long *responseCode) {
|
||||
if (CURLE_OK != curl_easy_perform(_curl))
|
||||
return false;
|
||||
CURLcode code = curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, responseCode);
|
||||
if (code != CURLE_OK || !(*responseCode >= 200 && *responseCode < 300)) {
|
||||
CC_LOG_ERROR("Curl curl_easy_getinfo failed: %s", curl_easy_strerror(code));
|
||||
return false;
|
||||
}
|
||||
// Get some mor data.
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
//Process Get Request
|
||||
static int processGetTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_FOLLOWLOCATION, true) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
//Process POST Request
|
||||
static int processPostTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_POST, 1) && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData()) && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize()) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
//Process PUT Request
|
||||
static int processPutTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_CUSTOMREQUEST, "PUT") && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData()) && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize()) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
//Process HEAD Request
|
||||
static int processHeadTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_NOBODY, "HEAD") && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData()) && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize()) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
//Process DELETE Request
|
||||
static int processDeleteTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_CUSTOMREQUEST, "DELETE") && curl.setOption(CURLOPT_FOLLOWLOCATION, true) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
//Process PATCH Request
|
||||
static int processPatchTask(HttpClient *client, HttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer) {
|
||||
CURLRaii curl;
|
||||
bool ok = curl.init(client, request, callback, stream, headerCallback, headerStream, errorBuffer) && curl.setOption(CURLOPT_CUSTOMREQUEST, "PATCH") && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData()) && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize()) && curl.perform(responseCode);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
// HttpClient implementation
|
||||
HttpClient *HttpClient::getInstance() {
|
||||
if (_httpClient == nullptr) {
|
||||
_httpClient = ccnew HttpClient();
|
||||
}
|
||||
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
void HttpClient::destroyInstance() {
|
||||
if (nullptr == _httpClient) {
|
||||
CC_LOG_DEBUG("HttpClient singleton is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance begin");
|
||||
auto thiz = _httpClient;
|
||||
_httpClient = nullptr;
|
||||
|
||||
if (auto sche = thiz->_scheduler.lock()) {
|
||||
sche->unscheduleAllForTarget(thiz);
|
||||
}
|
||||
|
||||
thiz->_schedulerMutex.lock();
|
||||
thiz->_scheduler.reset();
|
||||
thiz->_schedulerMutex.unlock();
|
||||
|
||||
thiz->_requestQueueMutex.lock();
|
||||
thiz->_requestQueue.pushBack(thiz->_requestSentinel);
|
||||
thiz->_requestQueueMutex.unlock();
|
||||
|
||||
thiz->_sleepCondition.notify_one();
|
||||
thiz->decreaseThreadCountAndMayDeleteThis();
|
||||
|
||||
CC_LOG_DEBUG("HttpClient::destroyInstance() finished!");
|
||||
}
|
||||
|
||||
void HttpClient::enableCookies(const char *cookieFile) {
|
||||
std::lock_guard<std::mutex> lock(_cookieFileMutex);
|
||||
if (cookieFile) {
|
||||
_cookieFilename = ccstd::string(cookieFile);
|
||||
} else {
|
||||
_cookieFilename = (FileUtils::getInstance()->getWritablePath() + "cookieFile.txt");
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::setSSLVerification(const ccstd::string &caFile) {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
_sslCaFilename = caFile;
|
||||
}
|
||||
|
||||
HttpClient::HttpClient()
|
||||
: _isInited(false),
|
||||
_timeoutForConnect(30),
|
||||
_timeoutForRead(60),
|
||||
_threadCount(0),
|
||||
_cookie(nullptr),
|
||||
_requestSentinel(ccnew HttpRequest()) {
|
||||
CC_LOG_DEBUG("In the constructor of HttpClient!");
|
||||
_requestSentinel->addRef();
|
||||
if (gThreadPool == nullptr) {
|
||||
gThreadPool = LegacyThreadPool::newFixedThreadPool(4);
|
||||
}
|
||||
memset(_responseMessage, 0, RESPONSE_BUFFER_SIZE * sizeof(char));
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
increaseThreadCount();
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient() {
|
||||
CC_SAFE_RELEASE(_requestSentinel);
|
||||
CC_LOG_DEBUG("HttpClient destructor");
|
||||
}
|
||||
|
||||
//Lazy create semaphore & mutex & thread
|
||||
bool HttpClient::lazyInitThreadSemaphore() {
|
||||
if (_isInited) {
|
||||
return true;
|
||||
} else {
|
||||
auto t = std::thread(CC_CALLBACK_0(HttpClient::networkThread, this));
|
||||
t.detach();
|
||||
_isInited = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//Add a get task to queue
|
||||
void HttpClient::send(HttpRequest *request) {
|
||||
if (false == lazyInitThreadSemaphore()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
|
||||
_requestQueueMutex.lock();
|
||||
_requestQueue.pushBack(request);
|
||||
_requestQueueMutex.unlock();
|
||||
|
||||
// Notify thread start to work
|
||||
_sleepCondition.notify_one();
|
||||
}
|
||||
|
||||
void HttpClient::sendImmediate(HttpRequest *request) {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->addRef();
|
||||
// Create a HttpResponse object, the default setting is http access failed
|
||||
HttpResponse *response = ccnew HttpResponse(request);
|
||||
response->addRef(); // NOTE: RefCounted object's reference count is changed to 0 now. so needs to addRef after ccnew.
|
||||
|
||||
gThreadPool->pushTask([this, request, response](int /*tid*/) { HttpClient::networkThreadAlone(request, response); });
|
||||
}
|
||||
|
||||
// Poll and notify main thread if responses exists in queue
|
||||
void HttpClient::dispatchResponseCallbacks() {
|
||||
// log("CCHttpClient::dispatchResponseCallbacks is running");
|
||||
//occurs when cocos thread fires but the network thread has already quited
|
||||
HttpResponse *response = nullptr;
|
||||
|
||||
_responseQueueMutex.lock();
|
||||
if (!_responseQueue.empty()) {
|
||||
response = _responseQueue.at(0);
|
||||
_responseQueue.erase(0);
|
||||
}
|
||||
_responseQueueMutex.unlock();
|
||||
|
||||
if (response) {
|
||||
HttpRequest *request = response->getHttpRequest();
|
||||
const ccHttpRequestCallback &callback = request->getResponseCallback();
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(this, response);
|
||||
}
|
||||
|
||||
response->release();
|
||||
// do not release in other thread
|
||||
request->release();
|
||||
}
|
||||
}
|
||||
|
||||
// Process Response
|
||||
void HttpClient::processResponse(HttpResponse *response, char *responseMessage) {
|
||||
auto request = response->getHttpRequest();
|
||||
long responseCode = -1;
|
||||
int retValue = 0;
|
||||
|
||||
// Process the request -> get response packet
|
||||
switch (request->getRequestType()) {
|
||||
case HttpRequest::Type::GET: // HTTP GET
|
||||
retValue = processGetTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::POST: // HTTP POST
|
||||
retValue = processPostTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PUT:
|
||||
retValue = processPutTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::HEAD:
|
||||
retValue = processHeadTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::DELETE:
|
||||
retValue = processDeleteTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
case HttpRequest::Type::PATCH:
|
||||
retValue = processPatchTask(this, request,
|
||||
writeData,
|
||||
response->getResponseData(),
|
||||
&responseCode,
|
||||
writeHeaderData,
|
||||
response->getResponseHeader(),
|
||||
responseMessage);
|
||||
break;
|
||||
|
||||
default:
|
||||
CC_ABORT();
|
||||
break;
|
||||
}
|
||||
|
||||
// write data to HttpResponse
|
||||
response->setResponseCode(responseCode);
|
||||
if (retValue != 0) {
|
||||
response->setSucceed(false);
|
||||
response->setErrorBuffer(responseMessage);
|
||||
} else {
|
||||
response->setSucceed(true);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::increaseThreadCount() {
|
||||
_threadCountMutex.lock();
|
||||
++_threadCount;
|
||||
_threadCountMutex.unlock();
|
||||
}
|
||||
|
||||
void HttpClient::decreaseThreadCountAndMayDeleteThis() {
|
||||
bool needDeleteThis = false;
|
||||
_threadCountMutex.lock();
|
||||
--_threadCount;
|
||||
if (0 == _threadCount) {
|
||||
needDeleteThis = true;
|
||||
}
|
||||
|
||||
_threadCountMutex.unlock();
|
||||
if (needDeleteThis) {
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getCookieFilename() {
|
||||
std::lock_guard<std::mutex> lock(_cookieFileMutex);
|
||||
return _cookieFilename;
|
||||
}
|
||||
|
||||
const ccstd::string &HttpClient::getSSLVerification() {
|
||||
std::lock_guard<std::mutex> lock(_sslCaFileMutex);
|
||||
return _sslCaFilename;
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
181
cocos/network/HttpClient.h
Normal file
181
cocos/network/HttpClient.h
Normal file
@@ -0,0 +1,181 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2012 greathqy
|
||||
Copyright (c) 2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include "base/RefVector.h"
|
||||
#include "network/HttpCookie.h"
|
||||
#include "network/HttpRequest.h"
|
||||
#include "network/HttpResponse.h"
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
class Scheduler;
|
||||
namespace network {
|
||||
|
||||
/** Singleton that handles asynchronous http requests.
|
||||
*
|
||||
* Once the request completed, a callback will issued in main thread when it provided during make request.
|
||||
*
|
||||
* @lua NA
|
||||
*/
|
||||
class CC_DLL HttpClient {
|
||||
public:
|
||||
/**
|
||||
* The buffer size of _responseMessage
|
||||
*/
|
||||
static const int RESPONSE_BUFFER_SIZE = 256;
|
||||
|
||||
/**
|
||||
* Get instance of HttpClient.
|
||||
*
|
||||
* @return the instance of HttpClient.
|
||||
*/
|
||||
static HttpClient *getInstance();
|
||||
|
||||
/**
|
||||
* Release the instance of HttpClient.
|
||||
*/
|
||||
static void destroyInstance();
|
||||
|
||||
/**
|
||||
* Enable cookie support.
|
||||
*
|
||||
* @param cookieFile the filepath of cookie file.
|
||||
*/
|
||||
void enableCookies(const char *cookieFile);
|
||||
|
||||
/**
|
||||
* Get the cookie filename
|
||||
*
|
||||
* @return the cookie filename
|
||||
*/
|
||||
const ccstd::string &getCookieFilename();
|
||||
|
||||
/**
|
||||
* Set root certificate path for SSL verification.
|
||||
*
|
||||
* @param caFile a full path of root certificate.if it is empty, SSL verification is disabled.
|
||||
*/
|
||||
void setSSLVerification(const ccstd::string &caFile);
|
||||
|
||||
/**
|
||||
* Get the ssl CA filename
|
||||
*
|
||||
* @return the ssl CA filename
|
||||
*/
|
||||
const ccstd::string &getSSLVerification();
|
||||
|
||||
/**
|
||||
* Add a get request to task queue
|
||||
*
|
||||
* @param request a HttpRequest object, which includes url, response callback etc.
|
||||
please make sure request->_requestData is clear before calling "send" here.
|
||||
*/
|
||||
void send(HttpRequest *request);
|
||||
|
||||
/**
|
||||
* Immediate send a request
|
||||
*
|
||||
* @param request a HttpRequest object, which includes url, response callback etc.
|
||||
please make sure request->_requestData is clear before calling "sendImmediate" here.
|
||||
*/
|
||||
void sendImmediate(HttpRequest *request);
|
||||
|
||||
HttpCookie *getCookie() const { return _cookie; }
|
||||
|
||||
std::mutex &getCookieFileMutex() { return _cookieFileMutex; }
|
||||
|
||||
std::mutex &getSSLCaFileMutex() { return _sslCaFileMutex; }
|
||||
|
||||
private:
|
||||
HttpClient();
|
||||
virtual ~HttpClient();
|
||||
bool init();
|
||||
|
||||
/**
|
||||
* Init pthread mutex, semaphore, and create new thread for http requests
|
||||
* @return bool
|
||||
*/
|
||||
bool lazyInitThreadSemaphore();
|
||||
void networkThread();
|
||||
void networkThreadAlone(HttpRequest *request, HttpResponse *response);
|
||||
/** Poll function called from main thread to dispatch callbacks when http requests finished **/
|
||||
void dispatchResponseCallbacks();
|
||||
|
||||
void processResponse(HttpResponse *response, char *responseMessage);
|
||||
void increaseThreadCount();
|
||||
void decreaseThreadCountAndMayDeleteThis();
|
||||
|
||||
private: // NOLINT(readability-redundant-access-specifiers)
|
||||
bool _isInited;
|
||||
|
||||
int _timeoutForConnect;
|
||||
std::mutex _timeoutForConnectMutex;
|
||||
|
||||
int _timeoutForRead;
|
||||
std::mutex _timeoutForReadMutex;
|
||||
|
||||
int _threadCount;
|
||||
std::mutex _threadCountMutex;
|
||||
|
||||
std::weak_ptr<Scheduler> _scheduler;
|
||||
std::mutex _schedulerMutex;
|
||||
|
||||
RefVector<HttpRequest *> _requestQueue;
|
||||
std::mutex _requestQueueMutex;
|
||||
|
||||
RefVector<HttpResponse *> _responseQueue;
|
||||
std::mutex _responseQueueMutex;
|
||||
|
||||
ccstd::string _cookieFilename;
|
||||
std::mutex _cookieFileMutex;
|
||||
|
||||
ccstd::string _sslCaFilename;
|
||||
std::mutex _sslCaFileMutex;
|
||||
|
||||
HttpCookie *_cookie;
|
||||
|
||||
std::condition_variable_any _sleepCondition;
|
||||
|
||||
char _responseMessage[RESPONSE_BUFFER_SIZE];
|
||||
|
||||
HttpRequest *_requestSentinel;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
147
cocos/network/HttpCookie.cpp
Normal file
147
cocos/network/HttpCookie.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "network/HttpCookie.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include "platform/FileUtils.h"
|
||||
|
||||
void HttpCookie::readFile() {
|
||||
ccstd::string inString = cc::FileUtils::getInstance()->getStringFromFile(_cookieFileName);
|
||||
if (!inString.empty()) {
|
||||
ccstd::vector<ccstd::string> cookiesVec;
|
||||
cookiesVec.clear();
|
||||
|
||||
std::stringstream stream(inString);
|
||||
ccstd::string item;
|
||||
while (std::getline(stream, item, '\n')) {
|
||||
cookiesVec.push_back(item);
|
||||
}
|
||||
|
||||
if (cookiesVec.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_cookies.clear();
|
||||
|
||||
for (auto &cookie : cookiesVec) {
|
||||
if (cookie.length() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cookie.find("#HttpOnly_") != ccstd::string::npos) {
|
||||
cookie = cookie.substr(10);
|
||||
}
|
||||
|
||||
if (cookie.at(0) == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
CookiesInfo co;
|
||||
std::stringstream streamInfo(cookie);
|
||||
ccstd::vector<ccstd::string> elems;
|
||||
ccstd::string elemsItem;
|
||||
|
||||
while (std::getline(streamInfo, elemsItem, '\t')) {
|
||||
elems.push_back(elemsItem);
|
||||
}
|
||||
|
||||
co.domain = elems[0];
|
||||
if (co.domain.at(0) == '.') {
|
||||
co.domain = co.domain.substr(1);
|
||||
}
|
||||
co.tailmatch = (strcmp("TRUE", elems[1].c_str()) != 0);
|
||||
co.path = elems[2];
|
||||
co.secure = (strcmp("TRUE", elems[3].c_str()) != 0);
|
||||
co.expires = elems[4];
|
||||
co.name = elems[5];
|
||||
co.value = elems[6];
|
||||
_cookies.push_back(co);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ccstd::vector<CookiesInfo> *HttpCookie::getCookies() const {
|
||||
return &_cookies;
|
||||
}
|
||||
|
||||
const CookiesInfo *HttpCookie::getMatchCookie(const ccstd::string &url) const {
|
||||
for (const auto &cookie : _cookies) {
|
||||
if (url.find(cookie.domain) != ccstd::string::npos) {
|
||||
return &cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void HttpCookie::updateOrAddCookie(CookiesInfo *cookie) {
|
||||
for (auto &iter : _cookies) {
|
||||
if (cookie->domain == iter.domain) {
|
||||
iter = *cookie;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_cookies.push_back(*cookie);
|
||||
}
|
||||
|
||||
void HttpCookie::writeFile() {
|
||||
FILE *out;
|
||||
out = fopen(_cookieFileName.c_str(), "w");
|
||||
fputs(
|
||||
"# Netscape HTTP Cookie File\n"
|
||||
"# http://curl.haxx.se/docs/http-cookies.html\n"
|
||||
"# This file was generated by cocos2d-x! Edit at your own risk.\n"
|
||||
"# Test cocos2d-x cookie write.\n\n",
|
||||
out);
|
||||
|
||||
ccstd::string line;
|
||||
for (auto &cookie : _cookies) {
|
||||
line.clear();
|
||||
line.append(cookie.domain);
|
||||
line.append(1, '\t');
|
||||
cookie.tailmatch ? line.append("TRUE") : line.append("FALSE");
|
||||
line.append(1, '\t');
|
||||
line.append(cookie.path);
|
||||
line.append(1, '\t');
|
||||
cookie.secure ? line.append("TRUE") : line.append("FALSE");
|
||||
line.append(1, '\t');
|
||||
line.append(cookie.expires);
|
||||
line.append(1, '\t');
|
||||
line.append(cookie.name);
|
||||
line.append(1, '\t');
|
||||
line.append(cookie.value);
|
||||
//line.append(1, '\n');
|
||||
|
||||
fprintf(out, "%s\n", line.c_str());
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
}
|
||||
|
||||
void HttpCookie::setCookieFileName(const ccstd::string &filename) {
|
||||
_cookieFileName = filename;
|
||||
}
|
||||
60
cocos/network/HttpCookie.h
Normal file
60
cocos/network/HttpCookie.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef HTTP_COOKIE_H
|
||||
#define HTTP_COOKIE_H
|
||||
/// @cond DO_NOT_SHOW
|
||||
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
struct CookiesInfo {
|
||||
ccstd::string domain;
|
||||
bool tailmatch;
|
||||
ccstd::string path;
|
||||
bool secure;
|
||||
ccstd::string name;
|
||||
ccstd::string value;
|
||||
ccstd::string expires;
|
||||
};
|
||||
|
||||
class HttpCookie {
|
||||
public:
|
||||
void readFile();
|
||||
|
||||
void writeFile();
|
||||
void setCookieFileName(const ccstd::string &fileName);
|
||||
|
||||
const ccstd::vector<CookiesInfo> *getCookies() const;
|
||||
const CookiesInfo *getMatchCookie(const ccstd::string &url) const;
|
||||
void updateOrAddCookie(CookiesInfo *cookie);
|
||||
|
||||
private:
|
||||
ccstd::string _cookieFileName;
|
||||
ccstd::vector<CookiesInfo> _cookies;
|
||||
};
|
||||
|
||||
/// @endcond
|
||||
#endif /* HTTP_COOKIE_H */
|
||||
268
cocos/network/HttpRequest.h
Normal file
268
cocos/network/HttpRequest.h
Normal file
@@ -0,0 +1,268 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2010-2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef __HTTP_REQUEST_H__
|
||||
#define __HTTP_REQUEST_H__
|
||||
|
||||
#include "base/Macros.h"
|
||||
#include "base/RefCounted.h"
|
||||
|
||||
#include <functional>
|
||||
#include "base/std/container/string.h"
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
class HttpClient;
|
||||
class HttpResponse;
|
||||
|
||||
using ccHttpRequestCallback = std::function<void(HttpClient *, HttpResponse *)>;
|
||||
|
||||
/**
|
||||
* Defines the object which users must packed for HttpClient::send(HttpRequest*) method.
|
||||
* Please refer to tests/test-cpp/Classes/ExtensionTest/NetworkTest/HttpClientTest.cpp as a sample
|
||||
* @since v2.0.2
|
||||
*
|
||||
* @lua NA
|
||||
*/
|
||||
|
||||
#if (CC_PLATFORM == CC_PLATFORM_WINRT)
|
||||
#ifdef DELETE
|
||||
#undef DELETE
|
||||
#endif
|
||||
#endif
|
||||
|
||||
class CC_DLL HttpRequest : public RefCounted {
|
||||
public:
|
||||
/**
|
||||
* The HttpRequest type enum used in the HttpRequest::setRequestType.
|
||||
*/
|
||||
enum class Type {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
HEAD,
|
||||
PATCH,
|
||||
UNKNOWN,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Because HttpRequest object will be used between UI thread and network thread,
|
||||
requestObj->autorelease() is forbidden to avoid crashes in AutoreleasePool
|
||||
new/retain/release still works, which means you need to release it manually
|
||||
Please refer to HttpRequestTest.cpp to find its usage.
|
||||
*/
|
||||
HttpRequest()
|
||||
: _callback(nullptr) {}
|
||||
|
||||
/** Destructor. */
|
||||
~HttpRequest() override = default;
|
||||
|
||||
// setter/getters for properties
|
||||
|
||||
/**
|
||||
* Set request type of HttpRequest object before being sent,now it support the enum value of HttpRequest::Type.
|
||||
*
|
||||
* @param type the request type.
|
||||
*/
|
||||
inline void setRequestType(Type type) {
|
||||
_requestType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request type of HttpRequest object.
|
||||
*
|
||||
* @return HttpRequest::Type.
|
||||
*/
|
||||
inline Type getRequestType() const {
|
||||
return _requestType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the url address of HttpRequest object.
|
||||
* The url value could be like these: "http://httpbin.org/ip" or "https://httpbin.org/get"
|
||||
*
|
||||
* @param url the string object.
|
||||
*/
|
||||
inline void setUrl(const ccstd::string &url) {
|
||||
_url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url address of HttpRequest object.
|
||||
*
|
||||
* @return const char* the pointer of _url.
|
||||
*/
|
||||
inline const char *getUrl() const {
|
||||
return _url.c_str();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request data of HttpRequest object.
|
||||
*
|
||||
* @param buffer the buffer of request data, it support binary data.
|
||||
* @param len the size of request data.
|
||||
*/
|
||||
inline void setRequestData(const char *buffer, size_t len) {
|
||||
_requestData.assign(buffer, buffer + len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request data pointer of HttpRequest object.
|
||||
*
|
||||
* @return char* the request data pointer.
|
||||
*/
|
||||
inline char *getRequestData() {
|
||||
if (!_requestData.empty()) {
|
||||
return _requestData.data();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of request data
|
||||
*
|
||||
* @return uint32_t the size of request data
|
||||
*/
|
||||
inline uint32_t getRequestDataSize() const {
|
||||
return static_cast<uint32_t>(_requestData.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a string tag to identify your request.
|
||||
* This tag can be found in HttpResponse->getHttpRequest->getTag().
|
||||
*
|
||||
* @param tag the string object.
|
||||
*/
|
||||
inline void setTag(const ccstd::string &tag) {
|
||||
_tag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string tag to identify the request.
|
||||
* The best practice is to use it in your MyClass::onMyHttpRequestCompleted(sender, HttpResponse*) callback.
|
||||
*
|
||||
* @return const char* the pointer of _tag
|
||||
*/
|
||||
inline const char *getTag() const {
|
||||
return _tag.c_str();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user-customed data of HttpRequest object.
|
||||
* You can attach a customed data in each request, and get it back in response callback.
|
||||
* But you need to new/delete the data pointer manully.
|
||||
*
|
||||
* @param userData the string pointer
|
||||
*/
|
||||
inline void setUserData(void *userData) {
|
||||
_userData = userData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user-customed data pointer which were pre-setted.
|
||||
* Don't forget to delete it. HttpClient/HttpResponse/HttpRequest will do nothing with this pointer.
|
||||
*
|
||||
* @return void* the pointer of user-customed data.
|
||||
*/
|
||||
inline void *getUserData() const {
|
||||
return _userData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response callback function of HttpRequest object.
|
||||
* When response come back, we would call _pCallback to process response data.
|
||||
*
|
||||
* @param callback the ccHttpRequestCallback function.
|
||||
*/
|
||||
inline void setResponseCallback(const ccHttpRequestCallback &callback) {
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ccHttpRequestCallback callback function.
|
||||
*
|
||||
* @return const ccHttpRequestCallback& ccHttpRequestCallback callback function.
|
||||
*/
|
||||
inline const ccHttpRequestCallback &getResponseCallback() const {
|
||||
return _callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom-defined headers.
|
||||
*
|
||||
* @param pHeaders the string vector of custom-defined headers.
|
||||
*/
|
||||
inline void setHeaders(const ccstd::vector<ccstd::string> &headers) {
|
||||
_headers = headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom headers.
|
||||
*
|
||||
* @return ccstd::vector<ccstd::string> the string vector of custom-defined headers.
|
||||
*/
|
||||
inline ccstd::vector<ccstd::string> getHeaders() const {
|
||||
return _headers;
|
||||
}
|
||||
|
||||
inline void setTimeout(float timeoutInSeconds) {
|
||||
_timeoutInSeconds = timeoutInSeconds;
|
||||
}
|
||||
|
||||
inline float getTimeout() const {
|
||||
return _timeoutInSeconds;
|
||||
}
|
||||
|
||||
protected:
|
||||
// properties
|
||||
Type _requestType{Type::UNKNOWN}; /// kHttpRequestGet, kHttpRequestPost or other enums
|
||||
ccstd::string _url; /// target url that this request is sent to
|
||||
ccstd::vector<char> _requestData; /// used for POST
|
||||
ccstd::string _tag; /// user defined tag, to identify different requests in response callback
|
||||
ccHttpRequestCallback _callback; /// C++11 style callbacks
|
||||
void *_userData{nullptr}; /// You can add your customed data here
|
||||
ccstd::vector<ccstd::string> _headers; /// custom http headers
|
||||
float _timeoutInSeconds{10.F};
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
|
||||
#endif //__HTTP_REQUEST_H__
|
||||
211
cocos/network/HttpResponse.h
Normal file
211
cocos/network/HttpResponse.h
Normal file
@@ -0,0 +1,211 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2010-2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef __HTTP_RESPONSE__
|
||||
#define __HTTP_RESPONSE__
|
||||
|
||||
#include "network/HttpRequest.h"
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
/**
|
||||
* @brief defines the object which users will receive at onHttpCompleted(sender, HttpResponse) callback.
|
||||
* Please refer to samples/TestCpp/Classes/ExtensionTest/NetworkTest/HttpClientTest.cpp as a sample.
|
||||
* @since v2.0.2.
|
||||
* @lua NA
|
||||
*/
|
||||
class CC_DLL HttpResponse : public cc::RefCounted {
|
||||
public:
|
||||
/**
|
||||
* Constructor, it's used by HttpClient internal, users don't need to create HttpResponse manually.
|
||||
* @param request the corresponding HttpRequest which leads to this response.
|
||||
*/
|
||||
HttpResponse(HttpRequest *request)
|
||||
: _pHttpRequest(request),
|
||||
_succeed(false),
|
||||
_responseDataString("") {
|
||||
if (_pHttpRequest) {
|
||||
_pHttpRequest->addRef();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor, it will be called in HttpClient internal.
|
||||
* Users don't need to destruct HttpResponse object manually.
|
||||
*/
|
||||
virtual ~HttpResponse() {
|
||||
if (_pHttpRequest) {
|
||||
_pHttpRequest->release();
|
||||
}
|
||||
}
|
||||
|
||||
// getters, will be called by users
|
||||
|
||||
/**
|
||||
* Get the corresponding HttpRequest object which leads to this response.
|
||||
* There's no paired setter for it, because it's already set in class constructor
|
||||
* @return HttpRequest* the corresponding HttpRequest object which leads to this response.
|
||||
*/
|
||||
inline HttpRequest *getHttpRequest() const {
|
||||
return _pHttpRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* To see if the http request is returned successfully.
|
||||
* Although users can judge if (http response code = 200), we want an easier way.
|
||||
* If this getter returns false, you can call getResponseCode and getErrorBuffer to find more details.
|
||||
* @return bool the flag that represent whether the http request return successfully or not.
|
||||
*/
|
||||
inline bool isSucceed() const {
|
||||
return _succeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the http response data.
|
||||
* @return ccstd::vector<char>* the pointer that point to the _responseData.
|
||||
*/
|
||||
inline ccstd::vector<char> *getResponseData() {
|
||||
return &_responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response headers.
|
||||
* @return ccstd::vector<char>* the pointer that point to the _responseHeader.
|
||||
*/
|
||||
inline ccstd::vector<char> *getResponseHeader() {
|
||||
return &_responseHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the http response code to judge whether response is successful or not.
|
||||
* I know that you want to see the _responseCode is 200.
|
||||
* If _responseCode is not 200, you should check the meaning for _responseCode by the net.
|
||||
* @return long the value of _responseCode
|
||||
*/
|
||||
inline long getResponseCode() const {
|
||||
return _responseCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error buffer which will tell you more about the reason why http request failed.
|
||||
* @return const char* the pointer that point to _errorBuffer.
|
||||
*/
|
||||
inline const char *getErrorBuffer() const {
|
||||
return _errorBuffer.c_str();
|
||||
}
|
||||
|
||||
// setters, will be called by HttpClient
|
||||
// users should avoid invoking these methods
|
||||
|
||||
/**
|
||||
* Set whether the http request is returned successfully or not,
|
||||
* This setter is mainly used in HttpClient, users mustn't set it directly
|
||||
* @param value the flag represent whether the http request is successful or not.
|
||||
*/
|
||||
inline void setSucceed(bool value) {
|
||||
_succeed = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the http response data buffer, it is used by HttpClient.
|
||||
* @param data the pointer point to the response data buffer.
|
||||
*/
|
||||
inline void setResponseData(ccstd::vector<char> *data) {
|
||||
_responseData = *data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the http response headers buffer, it is used by HttpClient.
|
||||
* @param data the pointer point to the response headers buffer.
|
||||
*/
|
||||
inline void setResponseHeader(ccstd::vector<char> *data) {
|
||||
_responseHeader = *data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the http response code.
|
||||
* @param value the http response code that represent whether the request is successful or not.
|
||||
*/
|
||||
inline void setResponseCode(long value) {
|
||||
_responseCode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error buffer which will tell you more the reason why http request failed.
|
||||
* @param value a string pointer that point to the reason.
|
||||
*/
|
||||
inline void setErrorBuffer(const char *value) {
|
||||
_errorBuffer.clear();
|
||||
if (value != nullptr)
|
||||
_errorBuffer.assign(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response data by the string pointer and the defined size.
|
||||
* @param value a string pointer that point to response data buffer.
|
||||
* @param n the defined size that the response data buffer would be copied.
|
||||
*/
|
||||
inline void setResponseDataString(const char *value, size_t n) {
|
||||
_responseDataString.clear();
|
||||
_responseDataString.assign(value, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string pointer that point to the response data.
|
||||
* @return const char* the string pointer that point to the response data.
|
||||
*/
|
||||
inline const char *getResponseDataString() const {
|
||||
return _responseDataString.c_str();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool initWithRequest(HttpRequest *request);
|
||||
|
||||
// properties
|
||||
HttpRequest *_pHttpRequest; /// the corresponding HttpRequest pointer who leads to this response
|
||||
bool _succeed; /// to indicate if the http request is successful simply
|
||||
ccstd::vector<char> _responseData; /// the returned raw data. You can also dump it as a string
|
||||
ccstd::vector<char> _responseHeader; /// the returned raw header data. You can also dump it as a string
|
||||
long _responseCode; /// the status code returned from libcurl, e.g. 200, 404
|
||||
ccstd::string _errorBuffer; /// if _responseCode != 200, please read _errorBuffer to find the reason
|
||||
ccstd::string _responseDataString; // the returned raw data. You can also dump it as a string
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
|
||||
#endif //__HTTP_RESPONSE_H__
|
||||
1069
cocos/network/SocketIO.cpp
Normal file
1069
cocos/network/SocketIO.cpp
Normal file
File diff suppressed because it is too large
Load Diff
261
cocos/network/SocketIO.h
Normal file
261
cocos/network/SocketIO.h
Normal file
@@ -0,0 +1,261 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2015 Chris Hannon http://www.channon.us
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include "base/Macros.h"
|
||||
#include "base/RefMap.h"
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/unordered_map.h"
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
//forward declarations
|
||||
class SIOClientImpl;
|
||||
class SIOClient;
|
||||
|
||||
/**
|
||||
* Singleton and wrapper class to provide static creation method as well as registry of all sockets.
|
||||
*
|
||||
* @lua NA
|
||||
*/
|
||||
class CC_DLL SocketIO {
|
||||
public:
|
||||
/**
|
||||
* Get instance of SocketIO.
|
||||
*
|
||||
* @return SocketIO* the instance of SocketIO.
|
||||
*/
|
||||
static SocketIO *getInstance();
|
||||
static void destroyInstance();
|
||||
|
||||
/**
|
||||
* The delegate class to process socket.io events.
|
||||
* @lua NA
|
||||
*/
|
||||
class SIODelegate {
|
||||
public:
|
||||
/** Destructor of SIODelegate. */
|
||||
virtual ~SIODelegate() = default;
|
||||
/**
|
||||
* This is kept for backwards compatibility, connect is now fired as a socket.io event "connect"
|
||||
*
|
||||
* This function would be called when the related SIOClient object receive messages that mean it have connected to endpoint successfully.
|
||||
*
|
||||
* @param client the connected SIOClient object.
|
||||
*/
|
||||
virtual void onConnect(SIOClient * /*client*/) { CC_LOG_DEBUG("SIODelegate onConnect fired"); };
|
||||
/**
|
||||
* This is kept for backwards compatibility, message is now fired as a socket.io event "message"
|
||||
*
|
||||
* This function would be called when the related SIOClient object receive message or json message.
|
||||
*
|
||||
* @param client the connected SIOClient object.
|
||||
* @param data the message,it could be json message
|
||||
*/
|
||||
virtual void onMessage(SIOClient * /*client*/, const ccstd::string &data) { CC_LOG_DEBUG("SIODelegate onMessage fired with data: %s", data.c_str()); };
|
||||
/**
|
||||
* Pure virtual callback function, this function should be overridden by the subclass.
|
||||
*
|
||||
* This function would be called when the related SIOClient object disconnect or receive disconnect signal.
|
||||
*
|
||||
* @param client the connected SIOClient object.
|
||||
*/
|
||||
virtual void onClose(SIOClient *client) = 0;
|
||||
/**
|
||||
* Pure virtual callback function, this function should be overridden by the subclass.
|
||||
*
|
||||
* This function would be called when the related SIOClient object receive error signal or didn't connect the endpoint but do some network operation, eg.,send and emit,etc.
|
||||
*
|
||||
* @param client the connected SIOClient object.
|
||||
* @param data the error message
|
||||
*/
|
||||
virtual void onError(SIOClient *client, const ccstd::string &data) = 0;
|
||||
/**
|
||||
* Fire event to script when the related SIOClient object receive the fire event signal.
|
||||
*
|
||||
* @param client the connected SIOClient object.
|
||||
* @param eventName the event's name.
|
||||
* @param data the event's data information.
|
||||
*/
|
||||
virtual void fireEventToScript(SIOClient * /*client*/, const ccstd::string &eventName, const ccstd::string &data) { CC_LOG_DEBUG("SIODelegate event '%s' fired with data: %s", eventName.c_str(), data.c_str()); };
|
||||
};
|
||||
|
||||
/**
|
||||
* Static client creation method, similar to socketio.connect(uri) in JS.
|
||||
* @param uri the URI of the socket.io server.
|
||||
* @param delegate the delegate which want to receive events from the socket.io client.
|
||||
* @return SIOClient* an initialized SIOClient if connected successfully, otherwise nullptr.
|
||||
*/
|
||||
static SIOClient *connect(const ccstd::string &uri, SIODelegate &delegate);
|
||||
|
||||
/**
|
||||
* Static client creation method, similar to socketio.connect(uri) in JS.
|
||||
* @param uri the URI of the socket.io server.
|
||||
* @param delegate the delegate which want to receive events from the socket.io client.
|
||||
* @param caFilePath The ca file path for wss connection
|
||||
* @return SIOClient* an initialized SIOClient if connected successfully, otherwise nullptr.
|
||||
*/
|
||||
static SIOClient *connect(const ccstd::string &uri, SIODelegate &delegate, const ccstd::string &caFilePath);
|
||||
|
||||
/**
|
||||
* Static client creation method, similar to socketio.connect(uri) in JS.
|
||||
* @param delegate the delegate which want to receive events from the socket.io client.
|
||||
* @param uri the URI of the socket.io server.
|
||||
* @return SIOClient* an initialized SIOClient if connected successfully, otherwise nullptr.
|
||||
*/
|
||||
CC_DEPRECATED_ATTRIBUTE static SIOClient *connect(SIODelegate &delegate, const ccstd::string &uri);
|
||||
|
||||
private:
|
||||
SocketIO();
|
||||
virtual ~SocketIO();
|
||||
|
||||
static SocketIO *inst;
|
||||
|
||||
cc::RefMap<ccstd::string, SIOClientImpl *> _sockets;
|
||||
|
||||
SIOClientImpl *getSocket(const ccstd::string &uri);
|
||||
void addSocket(const ccstd::string &uri, SIOClientImpl *socket);
|
||||
void removeSocket(const ccstd::string &uri);
|
||||
|
||||
friend class SIOClientImpl;
|
||||
|
||||
CC_DISALLOW_COPY_MOVE_ASSIGN(SocketIO)
|
||||
};
|
||||
|
||||
//c++11 style callbacks entities will be created using CC_CALLBACK (which uses std::bind)
|
||||
using SIOEvent = std::function<void(SIOClient *, const ccstd::string &)>;
|
||||
//c++11 map to callbacks
|
||||
using EventRegistry = ccstd::unordered_map<ccstd::string, SIOEvent>;
|
||||
|
||||
/**
|
||||
* A single connection to a socket.io endpoint.
|
||||
*
|
||||
* @lua NA
|
||||
*/
|
||||
class CC_DLL SIOClient
|
||||
: public cc::RefCounted {
|
||||
private:
|
||||
friend class SocketIO; // Only SocketIO class could contruct a SIOClient instance.
|
||||
|
||||
ccstd::string _path, _tag;
|
||||
bool _connected;
|
||||
SIOClientImpl *_socket;
|
||||
|
||||
SocketIO::SIODelegate *_delegate;
|
||||
|
||||
EventRegistry _eventRegistry;
|
||||
uint32_t _instanceId;
|
||||
|
||||
void fireEvent(const ccstd::string &eventName, const ccstd::string &data);
|
||||
|
||||
void onOpen();
|
||||
void onConnect();
|
||||
void socketClosed();
|
||||
|
||||
friend class SIOClientImpl;
|
||||
|
||||
/**
|
||||
* Constructor of SIOClient class.
|
||||
*
|
||||
* @param host the string that represent the host address.
|
||||
* @param port the int value represent the port number.
|
||||
* @param path the string that represent endpoint.
|
||||
* @param impl the SIOClientImpl object.
|
||||
* @param delegate the SIODelegate object.
|
||||
*/
|
||||
SIOClient(ccstd::string path, SIOClientImpl *impl, SocketIO::SIODelegate &delegate);
|
||||
/**
|
||||
* Destructor of SIOClient class.
|
||||
*/
|
||||
~SIOClient() override;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Get the delegate for the client
|
||||
* @return the delegate object for the client
|
||||
*/
|
||||
SocketIO::SIODelegate *getDelegate() { return _delegate; };
|
||||
|
||||
/**
|
||||
* Disconnect from the endpoint, onClose will be called for the delegate when complete
|
||||
*/
|
||||
void disconnect();
|
||||
/**
|
||||
* Send a message to the socket.io server.
|
||||
*
|
||||
* @param s message.
|
||||
*/
|
||||
void send(const ccstd::string &s);
|
||||
/**
|
||||
* Emit the eventname and the args to the endpoint that _path point to.
|
||||
* @param eventname
|
||||
* @param args
|
||||
*/
|
||||
void emit(const ccstd::string &eventname, const ccstd::string &args);
|
||||
/**
|
||||
* Used to register a socket.io event callback.
|
||||
* Event argument should be passed using CC_CALLBACK2(&Base::function, this).
|
||||
* @param eventName the name of event.
|
||||
* @param e the callback function.
|
||||
*/
|
||||
void on(const ccstd::string &eventName, SIOEvent e);
|
||||
|
||||
/**
|
||||
* Set tag of SIOClient.
|
||||
* The tag is used to distinguish the various SIOClient objects.
|
||||
* @param tag string object.
|
||||
*/
|
||||
void setTag(const char *tag);
|
||||
|
||||
/**
|
||||
* Get tag of SIOClient.
|
||||
* @return const char* the pointer point to the _tag.
|
||||
*/
|
||||
const char *getTag() {
|
||||
return _tag.c_str();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets instance id
|
||||
*/
|
||||
uint32_t getInstanceId() const;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
340
cocos/network/Uri.cpp
Normal file
340
cocos/network/Uri.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright 2017 Facebook, Inc.
|
||||
* Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Uri class is based on the original file here https://github.com/facebook/folly/blob/master/folly/Uri.cpp
|
||||
*/
|
||||
|
||||
#include "network/Uri.h"
|
||||
#include "base/Log.h" // For CC_LOG_ERROR macro
|
||||
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#undef LIKELY
|
||||
#undef UNLIKELY
|
||||
|
||||
#if defined(__GNUC__) && __GNUC__ >= 4
|
||||
#define LIKELY(x) (__builtin_expect((x), 1))
|
||||
#define UNLIKELY(x) (__builtin_expect((x), 0))
|
||||
#else
|
||||
#define LIKELY(x) (x)
|
||||
#define UNLIKELY(x) (x)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
ccstd::string toString(T arg) {
|
||||
std::stringstream ss;
|
||||
ss << arg;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
ccstd::string submatch(const std::smatch &m, int idx) {
|
||||
auto &sub = m[idx];
|
||||
return ccstd::string(sub.first, sub.second);
|
||||
}
|
||||
|
||||
template <class String>
|
||||
void toLower(String &s) {
|
||||
for (auto &c : s) {
|
||||
c = char(tolower(c));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
Uri::Uri()
|
||||
: _isValid(false),
|
||||
_isSecure(false),
|
||||
_hasAuthority(false),
|
||||
_port(0) {
|
||||
}
|
||||
|
||||
Uri::Uri(const Uri &o) {
|
||||
*this = o;
|
||||
}
|
||||
|
||||
Uri::Uri(Uri &&o) {
|
||||
*this = std::move(o);
|
||||
}
|
||||
|
||||
Uri &Uri::operator=(const Uri &o) {
|
||||
if (this != &o) {
|
||||
_isValid = o._isValid;
|
||||
_isSecure = o._isSecure;
|
||||
_scheme = o._scheme;
|
||||
_username = o._username;
|
||||
_password = o._password;
|
||||
_host = o._host;
|
||||
_hostName = o._hostName;
|
||||
_hasAuthority = o._hasAuthority;
|
||||
_port = o._port;
|
||||
_authority = o._authority;
|
||||
_pathEtc = o._pathEtc;
|
||||
_path = o._path;
|
||||
_query = o._query;
|
||||
_fragment = o._fragment;
|
||||
_queryParams = o._queryParams;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Uri &Uri::operator=(Uri &&o) {
|
||||
if (this != &o) {
|
||||
_isValid = o._isValid;
|
||||
o._isValid = false;
|
||||
_isSecure = o._isSecure;
|
||||
o._isSecure = false;
|
||||
_scheme = std::move(o._scheme);
|
||||
_username = std::move(o._username);
|
||||
_password = std::move(o._password);
|
||||
_host = std::move(o._host);
|
||||
_hostName = std::move(o._hostName);
|
||||
_hasAuthority = o._hasAuthority;
|
||||
o._hasAuthority = false;
|
||||
_port = o._port;
|
||||
o._port = 0;
|
||||
_authority = std::move(o._authority);
|
||||
_pathEtc = std::move(o._pathEtc);
|
||||
_path = std::move(o._path);
|
||||
_query = std::move(o._query);
|
||||
_fragment = std::move(o._fragment);
|
||||
_queryParams = std::move(o._queryParams);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Uri::operator==(const Uri &o) const {
|
||||
return (_isValid == o._isValid && _isSecure == o._isSecure && _scheme == o._scheme && _username == o._username && _password == o._password && _host == o._host && _hostName == o._hostName && _hasAuthority == o._hasAuthority && _port == o._port && _authority == o._authority && _pathEtc == o._pathEtc && _path == o._path && _query == o._query && _fragment == o._fragment && _queryParams == o._queryParams);
|
||||
}
|
||||
|
||||
Uri Uri::parse(const ccstd::string &str) {
|
||||
Uri uri;
|
||||
|
||||
if (!uri.doParse(str)) {
|
||||
uri.clear();
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
bool Uri::doParse(const ccstd::string &str) {
|
||||
static const std::regex uriRegex(
|
||||
"([a-zA-Z][a-zA-Z0-9+.-]*):" // scheme:
|
||||
"([^?#]*)" // authority and path
|
||||
"(?:\\?([^#]*))?" // ?query
|
||||
"(?:#(.*))?"); // #fragment
|
||||
static const std::regex authorityAndPathRegex("//([^/]*)(/.*)?");
|
||||
|
||||
if (str.empty()) {
|
||||
CC_LOG_ERROR("%s", "Empty URI is invalid!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasScheme = true;
|
||||
;
|
||||
ccstd::string copied(str);
|
||||
if (copied.find("://") == ccstd::string::npos) {
|
||||
hasScheme = false;
|
||||
copied.insert(0, "abc://"); // Just make regex happy.
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
if (UNLIKELY(!std::regex_match(copied.cbegin(), copied.cend(), match, uriRegex))) {
|
||||
CC_LOG_ERROR("Invalid URI: %s", str.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasScheme) {
|
||||
_scheme = submatch(match, 1);
|
||||
toLower(_scheme);
|
||||
if (_scheme == "https" || _scheme == "wss") {
|
||||
_isSecure = true;
|
||||
}
|
||||
}
|
||||
|
||||
ccstd::string authorityAndPath(match[2].first, match[2].second);
|
||||
std::smatch authorityAndPathMatch;
|
||||
if (!std::regex_match(authorityAndPath.cbegin(),
|
||||
authorityAndPath.cend(),
|
||||
authorityAndPathMatch,
|
||||
authorityAndPathRegex)) {
|
||||
// Does not start with //, doesn't have authority
|
||||
_hasAuthority = false;
|
||||
_path = authorityAndPath;
|
||||
} else {
|
||||
static const std::regex authorityRegex(
|
||||
"(?:([^@:]*)(?::([^@]*))?@)?" // username, password
|
||||
"(\\[[^\\]]*\\]|[^\\[:]*)" // host (IP-literal (e.g. '['+IPv6+']',
|
||||
// dotted-IPv4, or named host)
|
||||
"(?::(\\d*))?"); // port
|
||||
|
||||
auto authority = authorityAndPathMatch[1];
|
||||
std::smatch authorityMatch;
|
||||
if (!std::regex_match(authority.first,
|
||||
authority.second,
|
||||
authorityMatch,
|
||||
authorityRegex)) {
|
||||
ccstd::string invalidAuthority(authority.first, authority.second);
|
||||
CC_LOG_ERROR("Invalid URI authority: %s", invalidAuthority.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
ccstd::string port(authorityMatch[4].first, authorityMatch[4].second);
|
||||
if (!port.empty()) {
|
||||
_port = static_cast<uint16_t>(atoi(port.c_str()));
|
||||
}
|
||||
|
||||
_hasAuthority = true;
|
||||
_username = submatch(authorityMatch, 1);
|
||||
_password = submatch(authorityMatch, 2);
|
||||
_host = submatch(authorityMatch, 3);
|
||||
_path = submatch(authorityAndPathMatch, 2);
|
||||
}
|
||||
|
||||
_query = submatch(match, 3);
|
||||
_fragment = submatch(match, 4);
|
||||
_isValid = true;
|
||||
|
||||
// Assign authority part
|
||||
//
|
||||
// Port is 5 characters max and we have up to 3 delimiters.
|
||||
_authority.reserve(getHost().size() + getUserName().size() + getPassword().size() + 8);
|
||||
|
||||
if (!getUserName().empty() || !getPassword().empty()) {
|
||||
_authority.append(getUserName());
|
||||
|
||||
if (!getPassword().empty()) {
|
||||
_authority.push_back(':');
|
||||
_authority.append(getPassword());
|
||||
}
|
||||
|
||||
_authority.push_back('@');
|
||||
}
|
||||
|
||||
_authority.append(getHost());
|
||||
|
||||
if (getPort() != 0) {
|
||||
_authority.push_back(':');
|
||||
_authority.append(::toString(getPort()));
|
||||
}
|
||||
|
||||
// Assign path etc part
|
||||
_pathEtc = _path;
|
||||
if (!_query.empty()) {
|
||||
_pathEtc += '?';
|
||||
_pathEtc += _query;
|
||||
}
|
||||
|
||||
if (!_fragment.empty()) {
|
||||
_pathEtc += '#';
|
||||
_pathEtc += _fragment;
|
||||
}
|
||||
|
||||
// Assign host name
|
||||
if (!_host.empty() && _host[0] == '[') {
|
||||
// If it starts with '[', then it should end with ']', this is ensured by
|
||||
// regex
|
||||
_hostName = _host.substr(1, _host.size() - 2);
|
||||
} else {
|
||||
_hostName = _host;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Uri::clear() {
|
||||
_isValid = false;
|
||||
_isSecure = false;
|
||||
_scheme.clear();
|
||||
_username.clear();
|
||||
_password.clear();
|
||||
_host.clear();
|
||||
_hostName.clear();
|
||||
_hasAuthority = false;
|
||||
_port = 0;
|
||||
_authority.clear();
|
||||
_pathEtc.clear();
|
||||
_path.clear();
|
||||
_query.clear();
|
||||
_fragment.clear();
|
||||
_queryParams.clear();
|
||||
}
|
||||
|
||||
const ccstd::vector<std::pair<ccstd::string, ccstd::string>> &Uri::getQueryParams() {
|
||||
if (!_query.empty() && _queryParams.empty()) {
|
||||
// Parse query string
|
||||
static const std::regex queryParamRegex(
|
||||
"(^|&)" /*start of query or start of parameter "&"*/
|
||||
"([^=&]*)=?" /*parameter name and "=" if value is expected*/
|
||||
"([^=&]*)" /*parameter value*/
|
||||
"(?=(&|$))" /*forward reference, next should be end of query or
|
||||
start of next parameter*/
|
||||
);
|
||||
std::cregex_iterator paramBeginItr(
|
||||
_query.data(), _query.data() + _query.size(), queryParamRegex);
|
||||
std::cregex_iterator paramEndItr;
|
||||
for (auto itr = paramBeginItr; itr != paramEndItr; itr++) {
|
||||
if (itr->length(2) == 0) {
|
||||
// key is empty, ignore it
|
||||
continue;
|
||||
}
|
||||
_queryParams.emplace_back(
|
||||
ccstd::string((*itr)[2].first, (*itr)[2].second), // parameter name
|
||||
ccstd::string((*itr)[3].first, (*itr)[3].second) // parameter value
|
||||
);
|
||||
}
|
||||
}
|
||||
return _queryParams;
|
||||
}
|
||||
|
||||
ccstd::string Uri::toString() const {
|
||||
std::stringstream ss;
|
||||
if (_hasAuthority) {
|
||||
ss << _scheme << "://";
|
||||
if (!_password.empty()) {
|
||||
ss << _username << ":" << _password << "@";
|
||||
} else if (!_username.empty()) {
|
||||
ss << _username << "@";
|
||||
}
|
||||
ss << _host;
|
||||
if (_port != 0) {
|
||||
ss << ":" << _port;
|
||||
}
|
||||
} else {
|
||||
ss << _scheme << ":";
|
||||
}
|
||||
ss << _path;
|
||||
if (!_query.empty()) {
|
||||
ss << "?" << _query;
|
||||
}
|
||||
if (!_fragment.empty()) {
|
||||
ss << "#" << _fragment;
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
182
cocos/network/Uri.h
Normal file
182
cocos/network/Uri.h
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2017 Facebook, Inc.
|
||||
* Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Uri class is based on the original file here https://github.com/facebook/folly/blob/master/folly/Uri.cpp
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "base/Macros.h"
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
/**
|
||||
* Class representing a URI.
|
||||
*
|
||||
* Consider http://www.facebook.com/foo/bar?key=foo#anchor
|
||||
*
|
||||
* The URI is broken down into its parts: scheme ("http"), authority
|
||||
* (ie. host and port, in most cases: "www.facebook.com"), path
|
||||
* ("/foo/bar"), query ("key=foo") and fragment ("anchor"). The scheme is
|
||||
* lower-cased.
|
||||
*
|
||||
* If this Uri represents a URL, note that, to prevent ambiguity, the component
|
||||
* parts are NOT percent-decoded; you should do this yourself with
|
||||
* uriUnescape() (for the authority and path) and uriUnescape(...,
|
||||
* UriEscapeMode::QUERY) (for the query, but probably only after splitting at
|
||||
* '&' to identify the individual parameters).
|
||||
*/
|
||||
class CC_DLL Uri {
|
||||
public:
|
||||
/**
|
||||
* Parse a Uri from a string. Throws std::invalid_argument on parse error.
|
||||
*/
|
||||
static Uri parse(const ccstd::string &str);
|
||||
|
||||
/** Default constructor */
|
||||
Uri();
|
||||
|
||||
/** Copy constructor */
|
||||
Uri(const Uri &o);
|
||||
|
||||
/** Move constructor */
|
||||
Uri(Uri &&o);
|
||||
|
||||
/** Copy assignment */
|
||||
Uri &operator=(const Uri &o);
|
||||
|
||||
/** Move assignment */
|
||||
Uri &operator=(Uri &&o);
|
||||
|
||||
/** Checks whether two Uri instances contain the same values */
|
||||
bool operator==(const Uri &o) const;
|
||||
|
||||
/** Checks wether it's a valid URI */
|
||||
bool isValid() const { return _isValid; }
|
||||
|
||||
/** Checks whether it's a SSL connection */
|
||||
bool isSecure() const { return _isSecure; }
|
||||
|
||||
/** Gets the scheme name for this URI. */
|
||||
const ccstd::string &getScheme() const { return _scheme; }
|
||||
|
||||
/** Gets the user name with the specified URI. */
|
||||
const ccstd::string &getUserName() const { return _username; }
|
||||
|
||||
/** Gets the password with the specified URI. */
|
||||
const ccstd::string &getPassword() const { return _password; }
|
||||
/**
|
||||
* Get host part of URI. If host is an IPv6 address, square brackets will be
|
||||
* returned, for example: "[::1]".
|
||||
*/
|
||||
const ccstd::string &getHost() const { return _host; }
|
||||
/**
|
||||
* Get host part of URI. If host is an IPv6 address, square brackets will not
|
||||
* be returned, for exmaple "::1"; otherwise it returns the same thing as
|
||||
* getHost().
|
||||
*
|
||||
* getHostName() is what one needs to call if passing the host to any other tool
|
||||
* or API that connects to that host/port; e.g. getaddrinfo() only understands
|
||||
* IPv6 host without square brackets
|
||||
*/
|
||||
const ccstd::string &getHostName() const { return _hostName; }
|
||||
|
||||
/** Gets the port number of the URI. */
|
||||
uint16_t getPort() const { return _port; }
|
||||
|
||||
/** Gets the path part of the URI. */
|
||||
const ccstd::string &getPath() const { return _path; }
|
||||
|
||||
/// Gets the path, query and fragment parts of the URI.
|
||||
const ccstd::string &getPathEtc() const { return _pathEtc; }
|
||||
|
||||
/** Gets the query part of the URI. */
|
||||
const ccstd::string &getQuery() const { return _query; }
|
||||
|
||||
/** Gets the fragment part of the URI */
|
||||
const ccstd::string &getFragment() const { return _fragment; }
|
||||
|
||||
/** Gets the authority part (userName, password, host and port) of the URI.
|
||||
* @note If the port number is a well-known port
|
||||
* number for the given scheme (e.g., 80 for http), it
|
||||
* is not included in the authority.
|
||||
*/
|
||||
const ccstd::string &getAuthority() const { return _authority; }
|
||||
|
||||
/** Gets a string representation of the URI. */
|
||||
ccstd::string toString() const;
|
||||
|
||||
/**
|
||||
* Get query parameters as key-value pairs.
|
||||
* e.g. for URI containing query string: key1=foo&key2=&key3&=bar&=bar=
|
||||
* In returned list, there are 3 entries:
|
||||
* "key1" => "foo"
|
||||
* "key2" => ""
|
||||
* "key3" => ""
|
||||
* Parts "=bar" and "=bar=" are ignored, as they are not valid query
|
||||
* parameters. "=bar" is missing parameter name, while "=bar=" has more than
|
||||
* one equal signs, we don't know which one is the delimiter for key and
|
||||
* value.
|
||||
*
|
||||
* Note, this method is not thread safe, it might update internal state, but
|
||||
* only the first call to this method update the state. After the first call
|
||||
* is finished, subsequent calls to this method are thread safe.
|
||||
*
|
||||
* @return query parameter key-value pairs in a vector, each element is a
|
||||
* pair of which the first element is parameter name and the second
|
||||
* one is parameter value
|
||||
*/
|
||||
const ccstd::vector<std::pair<ccstd::string, ccstd::string>> &getQueryParams();
|
||||
|
||||
/** Clears all parts of the URI. */
|
||||
void clear();
|
||||
|
||||
private:
|
||||
bool doParse(const ccstd::string &str);
|
||||
|
||||
bool _isValid;
|
||||
bool _isSecure;
|
||||
ccstd::string _scheme;
|
||||
ccstd::string _username;
|
||||
ccstd::string _password;
|
||||
ccstd::string _host;
|
||||
ccstd::string _hostName;
|
||||
bool _hasAuthority;
|
||||
uint16_t _port;
|
||||
ccstd::string _authority;
|
||||
ccstd::string _pathEtc;
|
||||
ccstd::string _path;
|
||||
ccstd::string _query;
|
||||
ccstd::string _fragment;
|
||||
ccstd::vector<std::pair<ccstd::string, ccstd::string>> _queryParams;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
314
cocos/network/WebSocket-apple.mm
Normal file
314
cocos/network/WebSocket-apple.mm
Normal file
@@ -0,0 +1,314 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "base/Data.h"
|
||||
#include "network/WebSocket.h"
|
||||
|
||||
#import "SocketRocket/SocketRocket.h"
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
#error WebSocket must be compiled with ARC enabled
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
ccstd::vector<cc::network::WebSocket *> websocketInstances;
|
||||
}
|
||||
|
||||
@interface WebSocketImpl : NSObject <SRWebSocketDelegate> {
|
||||
}
|
||||
@end
|
||||
|
||||
//
|
||||
|
||||
@implementation WebSocketImpl {
|
||||
SRWebSocket *_ws;
|
||||
cc::network::WebSocket *_ccws;
|
||||
cc::network::WebSocket::Delegate *_delegate;
|
||||
|
||||
ccstd::string _url;
|
||||
ccstd::string _selectedProtocol;
|
||||
bool _isDestroyed;
|
||||
}
|
||||
|
||||
- (id)initWithURL:(const ccstd::string &)url protocols:(NSArray<NSString *> *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates ws:(cc::network::WebSocket *)ccws delegate:(const cc::network::WebSocket::Delegate &)delegate {
|
||||
if (self = [super init]) {
|
||||
_ccws = ccws;
|
||||
_delegate = const_cast<cc::network::WebSocket::Delegate *>(&delegate);
|
||||
_url = url;
|
||||
NSURL *nsUrl = [[NSURL alloc] initWithString:[[NSString alloc] initWithUTF8String:_url.c_str()]];
|
||||
_ws = [[SRWebSocket alloc] initWithURL:nsUrl protocols:protocols allowsUntrustedSSLCertificates:allowsUntrustedSSLCertificates];
|
||||
_ws.delegate = self;
|
||||
[_ws open];
|
||||
_isDestroyed = false;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
// NSLog(@"WebSocketImpl-apple dealloc: %p, SRWebSocket ref: %ld", self, CFGetRetainCount((__bridge CFTypeRef)_ws));
|
||||
}
|
||||
|
||||
- (void)sendString:(NSString *)message {
|
||||
[_ws sendString:message error:nil];
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data {
|
||||
[_ws sendData:data error:nil];
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
_isDestroyed = true;
|
||||
_ccws->addRef();
|
||||
_delegate->onClose(_ccws, 1000, "close_normal", true);
|
||||
[_ws close];
|
||||
_ccws->release();
|
||||
}
|
||||
|
||||
- (void)closeAsync {
|
||||
[_ws close];
|
||||
}
|
||||
|
||||
- (cc::network::WebSocket::State)getReadyState {
|
||||
cc::network::WebSocket::State ret;
|
||||
SRReadyState state = _ws.readyState;
|
||||
switch (state) {
|
||||
case SR_OPEN:
|
||||
ret = cc::network::WebSocket::State::OPEN;
|
||||
break;
|
||||
case SR_CONNECTING:
|
||||
ret = cc::network::WebSocket::State::CONNECTING;
|
||||
break;
|
||||
case SR_CLOSING:
|
||||
ret = cc::network::WebSocket::State::CLOSING;
|
||||
break;
|
||||
default:
|
||||
ret = cc::network::WebSocket::State::CLOSED;
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (const ccstd::string &)getUrl {
|
||||
return _url;
|
||||
}
|
||||
|
||||
- (const ccstd::string &)getProtocol {
|
||||
return _selectedProtocol;
|
||||
}
|
||||
|
||||
- (cc::network::WebSocket::Delegate *)getDelegate {
|
||||
return (cc::network::WebSocket::Delegate *)_delegate;
|
||||
}
|
||||
|
||||
// Delegate methods
|
||||
|
||||
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
|
||||
{
|
||||
if (!_isDestroyed) {
|
||||
// NSLog(@"Websocket Connected");
|
||||
if (webSocket.protocol != nil)
|
||||
_selectedProtocol = [webSocket.protocol UTF8String];
|
||||
_delegate->onOpen(_ccws);
|
||||
} else {
|
||||
NSLog(@"WebSocketImpl webSocketDidOpen was destroyed!");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
|
||||
{
|
||||
if (!_isDestroyed) {
|
||||
NSLog(@":( Websocket Failed With Error %@", error);
|
||||
_delegate->onError(_ccws, cc::network::WebSocket::ErrorCode::UNKNOWN);
|
||||
[self webSocket:webSocket didCloseWithCode:0 reason:@"onerror" wasClean:YES];
|
||||
} else {
|
||||
NSLog(@"WebSocketImpl didFailWithError was destroyed!");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessageWithString:(nonnull NSString *)string {
|
||||
if (!_isDestroyed) {
|
||||
cc::network::WebSocket::Data data;
|
||||
data.bytes = const_cast<char *>([string cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
data.len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
||||
data.isBinary = false;
|
||||
data.issued = 0;
|
||||
data.ext = nullptr;
|
||||
|
||||
_delegate->onMessage(_ccws, data);
|
||||
} else {
|
||||
NSLog(@"WebSocketImpl didReceiveMessageWithString was destroyed!");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessageWithData:(NSData *)nsData {
|
||||
if (!_isDestroyed) {
|
||||
cc::network::WebSocket::Data data;
|
||||
data.bytes = (char *)nsData.bytes;
|
||||
data.len = nsData.length;
|
||||
data.isBinary = true;
|
||||
data.issued = 0;
|
||||
data.ext = nullptr;
|
||||
_delegate->onMessage(_ccws, data);
|
||||
} else {
|
||||
NSLog(@"WebSocketImpl didReceiveMessageWithData was destroyed!");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
|
||||
if (!_isDestroyed) {
|
||||
auto codeArg = static_cast<uint16_t>(code);
|
||||
ccstd::string reasonArg = reason == nil ? "no_resaon" : ccstd::string([reason UTF8String]);
|
||||
bool wasCleanArg = static_cast<bool>(wasClean);
|
||||
_delegate->onClose(_ccws, codeArg, reasonArg, wasCleanArg);
|
||||
}
|
||||
else
|
||||
NSLog(@"WebSocketImpl didCloseWithCode was destroyed!");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
void WebSocket::closeAllConnections() {
|
||||
if (websocketInstances.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto iter = websocketInstances.cbegin(); iter != websocketInstances.cend(); ++iter) {
|
||||
(*iter)->close();
|
||||
++iter;
|
||||
}
|
||||
|
||||
websocketInstances.clear();
|
||||
}
|
||||
|
||||
WebSocket::WebSocket() {
|
||||
websocketInstances.push_back(this);
|
||||
}
|
||||
|
||||
WebSocket::~WebSocket() {
|
||||
// NSLog(@"In the destructor of WebSocket-apple (%p).", this);
|
||||
if (websocketInstances.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto iter = std::find(websocketInstances.begin(), websocketInstances.end(), this);
|
||||
if (iter != websocketInstances.end()) {
|
||||
websocketInstances.erase(iter);
|
||||
} else {
|
||||
NSLog(@"ERROR: WebSocket instance wasn't added to the container which saves websocket instances!");
|
||||
}
|
||||
}
|
||||
|
||||
bool WebSocket::init(const Delegate &delegate,
|
||||
const ccstd::string &url,
|
||||
const ccstd::vector<ccstd::string> *protocols /* = nullptr*/,
|
||||
const ccstd::string &caFilePath /* = ""*/) {
|
||||
if (url.empty())
|
||||
return false;
|
||||
|
||||
NSMutableArray *nsProtocols = [[NSMutableArray alloc] init];
|
||||
if (protocols != nullptr) {
|
||||
for (const auto &protocol : *protocols) {
|
||||
[nsProtocols addObject:[[NSString alloc] initWithUTF8String:protocol.c_str()]];
|
||||
}
|
||||
}
|
||||
_impl = [[WebSocketImpl alloc] initWithURL:url protocols:nsProtocols allowsUntrustedSSLCertificates:NO ws:this delegate:delegate];
|
||||
|
||||
return _impl != nil;
|
||||
}
|
||||
|
||||
void WebSocket::send(const ccstd::string &message) {
|
||||
if ([_impl getReadyState] == State::OPEN) {
|
||||
NSString *str = [[NSString alloc] initWithBytes:message.data() length:message.length() encoding:NSUTF8StringEncoding];
|
||||
[_impl sendString:str];
|
||||
} else {
|
||||
NSLog(@"Couldn't send message since websocket wasn't opened!");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocket::send(const unsigned char *binaryMsg, unsigned int len) {
|
||||
if ([_impl getReadyState] == State::OPEN) {
|
||||
NSData *data = [[NSData alloc] initWithBytes:binaryMsg length:(NSUInteger)len];
|
||||
[_impl sendData:data];
|
||||
} else {
|
||||
NSLog(@"Couldn't send message since websocket wasn't opened!");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocket::close() {
|
||||
if ([_impl getReadyState] == State::CLOSING || [_impl getReadyState] == State::CLOSED) {
|
||||
NSLog(@"WebSocket (%p) was closed, no need to close it again!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
[_impl close];
|
||||
}
|
||||
|
||||
void WebSocket::closeAsync() {
|
||||
if ([_impl getReadyState] == State::CLOSING || [_impl getReadyState] == State::CLOSED) {
|
||||
NSLog(@"WebSocket (%p) was closed, no need to close it again!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
[_impl closeAsync];
|
||||
}
|
||||
|
||||
void WebSocket::closeAsync(int code, const ccstd::string &reason) {
|
||||
//lws_close_reason() replacement required
|
||||
closeAsync();
|
||||
}
|
||||
|
||||
ccstd::string WebSocket::getExtensions() const {
|
||||
//TODO websocket extensions
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t WebSocket::getBufferedAmount() const {
|
||||
//TODO pending send bytes
|
||||
return 0;
|
||||
}
|
||||
|
||||
WebSocket::State WebSocket::getReadyState() const {
|
||||
return [_impl getReadyState];
|
||||
}
|
||||
|
||||
const ccstd::string &WebSocket::getUrl() const {
|
||||
return [_impl getUrl];
|
||||
}
|
||||
|
||||
const ccstd::string &WebSocket::getProtocol() const {
|
||||
return [_impl getProtocol];
|
||||
}
|
||||
|
||||
WebSocket::Delegate *WebSocket::getDelegate() const {
|
||||
return [_impl getDelegate];
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
1406
cocos/network/WebSocket-libwebsockets.cpp
Normal file
1406
cocos/network/WebSocket-libwebsockets.cpp
Normal file
File diff suppressed because it is too large
Load Diff
436
cocos/network/WebSocket-okhttp.cpp
Normal file
436
cocos/network/WebSocket-okhttp.cpp
Normal file
@@ -0,0 +1,436 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2022-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
https://www.cocos.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include <atomic>
|
||||
#include "WebSocket.h"
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/UTF8.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "platform/FileUtils.h"
|
||||
#include "platform/java/jni/JniHelper.h"
|
||||
|
||||
#ifdef JAVA_CLASS_WEBSOCKET
|
||||
#error "JAVA_CLASS_WEBSOCKET is already defined"
|
||||
#endif
|
||||
#ifdef HANDLE_TO_WS_OKHTTP3
|
||||
#error "HANDLE_TO_WS_OKHTTP3 is already defined"
|
||||
#endif
|
||||
|
||||
#define JAVA_CLASS_WEBSOCKET "com/cocos/lib/websocket/CocosWebSocket"
|
||||
#define HANDLE_TO_WS_OKHTTP3(handler) \
|
||||
reinterpret_cast<WebSocketImpl *>(static_cast<uintptr_t>(handler))
|
||||
|
||||
namespace {
|
||||
//NOLINTNEXTLINE
|
||||
void split_string(const std::string &s, std::vector<std::string> &v, const std::string &c) {
|
||||
v.clear();
|
||||
std::string::size_type pos1;
|
||||
std::string::size_type pos2;
|
||||
pos2 = s.find(c);
|
||||
pos1 = 0;
|
||||
while (std::string::npos != pos2) {
|
||||
v.push_back(s.substr(pos1, pos2 - pos1));
|
||||
|
||||
pos1 = pos2 + c.size();
|
||||
pos2 = s.find(c, pos1);
|
||||
}
|
||||
if (pos1 != s.length()) {
|
||||
v.push_back(s.substr(pos1));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
using cc::JniHelper;
|
||||
using cc::network::WebSocket;
|
||||
class WebSocketImpl final {
|
||||
public:
|
||||
static const char *connectID;
|
||||
static const char *removeHandlerID;
|
||||
static const char *sendBinaryID;
|
||||
static const char *sendStringID;
|
||||
static const char *closeID;
|
||||
static const char *getBufferedAmountID;
|
||||
static std::atomic_int64_t idGenerator;
|
||||
static std::unordered_map<int64_t, WebSocketImpl *> allConnections;
|
||||
|
||||
static void closeAllConnections();
|
||||
|
||||
explicit WebSocketImpl(cc::network::WebSocket *websocket);
|
||||
~WebSocketImpl();
|
||||
|
||||
bool init(const cc::network::WebSocket::Delegate &delegate,
|
||||
const std::string &url,
|
||||
const std::vector<std::string> *protocols = nullptr,
|
||||
const std::string &caFilePath = "");
|
||||
|
||||
void send(const std::string &message);
|
||||
void send(const unsigned char *binaryMsg, unsigned int len);
|
||||
void close();
|
||||
void closeAsync();
|
||||
void closeAsync(int code, const std::string &reason);
|
||||
cc::network::WebSocket::State getReadyState() const { return _readyState; }
|
||||
const std::string &getUrl() const { return _url; }
|
||||
const std::string &getProtocol() const { return _protocolString; }
|
||||
cc::network::WebSocket::Delegate *getDelegate() const { return _delegate; }
|
||||
|
||||
size_t getBufferedAmount() const;
|
||||
std::string getExtensions() const { return _extensions; }
|
||||
|
||||
void onOpen(const std::string &protocol, const std::string &headers);
|
||||
void onClose(int code, const std::string &reason, bool wasClean);
|
||||
void onError(int code, const std::string &reason);
|
||||
void onStringMessage(const std::string &message);
|
||||
void onBinaryMessage(const uint8_t *buf, size_t len);
|
||||
|
||||
private:
|
||||
WebSocket *_socket{nullptr};
|
||||
WebSocket::Delegate *_delegate{nullptr};
|
||||
jobject _javaSocket{nullptr};
|
||||
int64_t _identifier{0};
|
||||
std::string _protocolString;
|
||||
std::string _selectedProtocol;
|
||||
std::string _url;
|
||||
std::string _extensions;
|
||||
WebSocket::State _readyState{WebSocket::State::CONNECTING};
|
||||
std::unordered_map<std::string, std::string> _headerMap{};
|
||||
};
|
||||
|
||||
const char *WebSocketImpl::connectID = "_connect";
|
||||
const char *WebSocketImpl::removeHandlerID = "_removeHander";
|
||||
const char *WebSocketImpl::sendBinaryID = "_send";
|
||||
const char *WebSocketImpl::sendStringID = "_send";
|
||||
const char *WebSocketImpl::closeID = "_close";
|
||||
const char *WebSocketImpl::getBufferedAmountID = "_getBufferedAmountID";
|
||||
std::atomic_int64_t WebSocketImpl::idGenerator{0};
|
||||
std::unordered_map<int64_t, WebSocketImpl *> WebSocketImpl::allConnections{};
|
||||
|
||||
void WebSocketImpl::closeAllConnections() {
|
||||
std::unordered_map<int64_t, WebSocketImpl *> tmp = std::move(allConnections);
|
||||
for (auto &t : tmp) {
|
||||
t.second->closeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketImpl::WebSocketImpl(WebSocket *websocket) : _socket(websocket) {
|
||||
_identifier = idGenerator.fetch_add(1);
|
||||
allConnections.emplace(_identifier, this);
|
||||
}
|
||||
|
||||
WebSocketImpl::~WebSocketImpl() {
|
||||
auto *env = JniHelper::getEnv();
|
||||
JniHelper::getEnv()->DeleteGlobalRef(_javaSocket);
|
||||
_javaSocket = nullptr;
|
||||
allConnections.erase(_identifier);
|
||||
}
|
||||
|
||||
bool WebSocketImpl::init(const cc::network::WebSocket::Delegate &delegate, const std::string &url,
|
||||
const std::vector<std::string> *protocols, const std::string &caFilePath) {
|
||||
auto *env = JniHelper::getEnv();
|
||||
auto handler = static_cast<int64_t>(reinterpret_cast<uintptr_t>(this));
|
||||
bool tcpNoDelay = false;
|
||||
bool perMessageDeflate = true;
|
||||
int64_t timeout = 60 * 60 * 1000 /*ms*/; //TODO(PatriceJiang): set timeout
|
||||
std::vector<std::string> headers; //TODO(PatriceJiang): allow set headers
|
||||
_url = url;
|
||||
_delegate = const_cast<WebSocket::Delegate *>(&delegate);
|
||||
if (protocols != nullptr && !protocols->empty()) {
|
||||
// protocol should add to Request Header as part of the original handshake for key
|
||||
// Sec-WebSocket-Protocol, use ',' to separate more than one.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
||||
std::string item;
|
||||
auto it = protocols->begin();
|
||||
while (it != protocols->end()) {
|
||||
item = *it++;
|
||||
_protocolString.append(item);
|
||||
if (it != protocols->end()) {
|
||||
_protocolString.append(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
// header
|
||||
jobject jObj = JniHelper::newObject(JAVA_CLASS_WEBSOCKET, _identifier, handler, headers, tcpNoDelay, perMessageDeflate, timeout);
|
||||
_javaSocket = env->NewGlobalRef(jObj);
|
||||
JniHelper::callObjectVoidMethod(jObj, JAVA_CLASS_WEBSOCKET, connectID, url, _protocolString, caFilePath);
|
||||
env->DeleteLocalRef(jObj);
|
||||
_readyState = WebSocket::State::CONNECTING;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketImpl::send(const std::string &message) {
|
||||
if (_readyState == WebSocket::State::OPEN) {
|
||||
JniHelper::callObjectVoidMethod(_javaSocket, JAVA_CLASS_WEBSOCKET, sendStringID, message);
|
||||
} else {
|
||||
CC_LOG_ERROR("Couldn't send message since WebSocket wasn't opened!");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketImpl::send(const unsigned char *binaryMsg, unsigned int len) {
|
||||
if (_readyState == WebSocket::State::OPEN) {
|
||||
JniHelper::callObjectVoidMethod(_javaSocket, JAVA_CLASS_WEBSOCKET, sendBinaryID, std::make_pair(binaryMsg, static_cast<size_t>(len)));
|
||||
} else {
|
||||
CC_LOG_ERROR("Couldn't send message since WebSocket wasn't opened!");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketImpl::close() {
|
||||
closeAsync(); // close only run in async mode
|
||||
}
|
||||
|
||||
void WebSocketImpl::closeAsync() {
|
||||
closeAsync(1000, "normal closure");
|
||||
}
|
||||
|
||||
void WebSocketImpl::closeAsync(int code, const std::string &reason) {
|
||||
CC_LOG_DEBUG("close WebSocket: %p, code: %d, reason: %s",
|
||||
this,
|
||||
code,
|
||||
reason.c_str());
|
||||
if (_readyState == WebSocket::State::CLOSED ||
|
||||
_readyState == WebSocket::State::CLOSING) {
|
||||
CC_LOG_ERROR("close: WebSocket (%p) was closed, no need to close it again!", this);
|
||||
return;
|
||||
}
|
||||
_readyState = WebSocket::State::CLOSING; // update state -> CLOSING
|
||||
JniHelper::callObjectVoidMethod(_javaSocket, JAVA_CLASS_WEBSOCKET, closeID, code, reason);
|
||||
}
|
||||
|
||||
size_t WebSocketImpl::getBufferedAmount() const {
|
||||
jlong buffAmount = JniHelper::callObjectLongMethod(_javaSocket, JAVA_CLASS_WEBSOCKET, getBufferedAmountID);
|
||||
return static_cast<size_t>(buffAmount);
|
||||
}
|
||||
|
||||
void WebSocketImpl::onOpen(const std::string &protocol, const std::string &headers) {
|
||||
_selectedProtocol = protocol;
|
||||
std::vector<std::string> headerTokens;
|
||||
split_string(headers, headerTokens, "\n");
|
||||
std::vector<std::string> headerKV;
|
||||
for (auto &kv : headerTokens) {
|
||||
split_string(kv, headerKV, ": ");
|
||||
_headerMap.insert(std::pair<std::string, std::string>(headerKV[0],
|
||||
headerKV[1]));
|
||||
if (headerKV[0] == "Sec-WebSocket-Extensions") {
|
||||
_extensions = headerKV[1];
|
||||
}
|
||||
}
|
||||
if (_readyState == WebSocket::State::CLOSING || _readyState == WebSocket::State::CLOSED) {
|
||||
CC_LOG_DEBUG("websocket is closing");
|
||||
} else {
|
||||
_readyState = WebSocket::State::OPEN; // update state -> OPEN
|
||||
_delegate->onOpen(_socket);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketImpl::onClose(int code, const std::string &reason, bool wasClean) {
|
||||
_readyState = WebSocket::State::CLOSED; // update state -> CLOSED
|
||||
_delegate->onClose(_socket, code, reason, wasClean);
|
||||
}
|
||||
|
||||
void WebSocketImpl::onError(int code, const std::string &reason) {
|
||||
CC_LOG_DEBUG("WebSocket (%p) onError, state: %d ...", this, (int)_readyState);
|
||||
if (_readyState != WebSocket::State::CLOSED) {
|
||||
_readyState = WebSocket::State::CLOSED; // update state -> CLOSED
|
||||
_delegate->onError(_socket, static_cast<WebSocket::ErrorCode>(code));
|
||||
}
|
||||
onClose(code, reason, false);
|
||||
}
|
||||
|
||||
void WebSocketImpl::onBinaryMessage(const uint8_t *buf, size_t len) {
|
||||
WebSocket::Data data;
|
||||
data.bytes = reinterpret_cast<char *>(const_cast<uint8_t *>(buf));
|
||||
data.len = static_cast<ssize_t>(len);
|
||||
data.isBinary = true;
|
||||
_delegate->onMessage(_socket, data);
|
||||
}
|
||||
|
||||
void WebSocketImpl::onStringMessage(const std::string &message) {
|
||||
WebSocket::Data data;
|
||||
data.bytes = const_cast<char *>(message.c_str());
|
||||
data.len = static_cast<ssize_t>(message.length());
|
||||
data.isBinary = false;
|
||||
_delegate->onMessage(_socket, data);
|
||||
}
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
/*static*/
|
||||
void WebSocket::closeAllConnections() {
|
||||
WebSocketImpl::closeAllConnections();
|
||||
}
|
||||
|
||||
WebSocket::WebSocket() {
|
||||
_impl = ccnew WebSocketImpl(this);
|
||||
}
|
||||
|
||||
WebSocket::~WebSocket() {
|
||||
delete _impl;
|
||||
}
|
||||
|
||||
bool WebSocket::init(const Delegate &delegate,
|
||||
const std::string &url,
|
||||
const std::vector<std::string> *protocols /* = nullptr*/,
|
||||
const std::string &caFilePath /* = ""*/) {
|
||||
return _impl->init(delegate, url, protocols, caFilePath);
|
||||
}
|
||||
|
||||
void WebSocket::send(const std::string &message) {
|
||||
_impl->send(message);
|
||||
}
|
||||
|
||||
void WebSocket::send(const unsigned char *binaryMsg, unsigned int len) {
|
||||
_impl->send(binaryMsg, len);
|
||||
}
|
||||
|
||||
void WebSocket::close() {
|
||||
_impl->close();
|
||||
}
|
||||
|
||||
void WebSocket::closeAsync() {
|
||||
_impl->closeAsync();
|
||||
}
|
||||
void WebSocket::closeAsync(int code, const std::string &reason) {
|
||||
_impl->closeAsync(code, reason);
|
||||
}
|
||||
|
||||
WebSocket::State WebSocket::getReadyState() const {
|
||||
return _impl->getReadyState();
|
||||
}
|
||||
|
||||
std::string WebSocket::getExtensions() const {
|
||||
return _impl->getExtensions();
|
||||
}
|
||||
|
||||
size_t WebSocket::getBufferedAmount() const {
|
||||
return _impl->getBufferedAmount();
|
||||
}
|
||||
|
||||
const std::string &WebSocket::getUrl() const {
|
||||
return _impl->getUrl();
|
||||
}
|
||||
|
||||
const std::string &WebSocket::getProtocol() const {
|
||||
return _impl->getProtocol();
|
||||
}
|
||||
|
||||
WebSocket::Delegate *WebSocket::getDelegate() const {
|
||||
return _impl->getDelegate();
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
|
||||
extern "C" {
|
||||
#ifdef JNI_PATH
|
||||
#error "JNI_PATH is already defined"
|
||||
#endif
|
||||
|
||||
#ifdef RUN_IN_GAMETHREAD
|
||||
#error "RUN_IN_GAMETHREAD is already defined"
|
||||
#endif
|
||||
|
||||
#define JNI_PATH(methodName) Java_com_cocos_lib_websocket_CocosWebSocket_##methodName
|
||||
#define RUN_IN_GAMETHREAD(task) \
|
||||
do { \
|
||||
if (CC_CURRENT_APPLICATION()) { \
|
||||
CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=]() { \
|
||||
task; \
|
||||
}); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
JNIEXPORT void JNICALL JNI_PATH(NativeInit)(JNIEnv * /*env*/, jclass /*clazz*/) {
|
||||
// nop
|
||||
// may cache jni objects in the future
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
JNI_PATH(nativeOnStringMessage)(JNIEnv * /*env*/,
|
||||
jobject /*ctx*/,
|
||||
jstring msg,
|
||||
jlong /*identifier*/,
|
||||
jlong handler) {
|
||||
auto *wsOkHttp3 = HANDLE_TO_WS_OKHTTP3(handler); // NOLINT(performance-no-int-to-ptr)
|
||||
std::string msgStr = JniHelper::jstring2string(msg);
|
||||
RUN_IN_GAMETHREAD(wsOkHttp3->onStringMessage(msgStr));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
JNI_PATH(nativeOnBinaryMessage)(JNIEnv *env,
|
||||
jobject /*ctx*/,
|
||||
jbyteArray msg,
|
||||
jlong /*identifier*/,
|
||||
jlong handler) {
|
||||
auto *wsOkHttp3 = HANDLE_TO_WS_OKHTTP3(handler); // NOLINT(performance-no-int-to-ptr)
|
||||
jobject strongRef = env->NewGlobalRef(msg);
|
||||
RUN_IN_GAMETHREAD(do {
|
||||
auto *env = JniHelper::getEnv();
|
||||
auto len = env->GetArrayLength(static_cast<jbyteArray>(strongRef));
|
||||
jboolean isCopy = JNI_FALSE;
|
||||
jbyte *array = env->GetByteArrayElements(static_cast<jbyteArray>(strongRef), &isCopy);
|
||||
wsOkHttp3->onBinaryMessage(reinterpret_cast<uint8_t *>(array), len);
|
||||
env->DeleteGlobalRef(strongRef);
|
||||
} while (false));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
JNI_PATH(nativeOnOpen)(JNIEnv * /*env*/,
|
||||
jobject /*ctx*/,
|
||||
jstring protocol,
|
||||
jstring header,
|
||||
jlong /*identifier*/,
|
||||
jlong handler) {
|
||||
auto *wsOkHttp3 = HANDLE_TO_WS_OKHTTP3(handler); // NOLINT(performance-no-int-to-ptr)
|
||||
auto protocolStr = JniHelper::jstring2string(protocol);
|
||||
auto headerStr = JniHelper::jstring2string(header);
|
||||
RUN_IN_GAMETHREAD(wsOkHttp3->onOpen(protocolStr, headerStr));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
JNI_PATH(nativeOnClosed)(JNIEnv * /*env*/,
|
||||
jobject /*ctx*/,
|
||||
jint code,
|
||||
jstring reason,
|
||||
jlong /*identifier*/,
|
||||
jlong handler) {
|
||||
auto *wsOkHttp3 = HANDLE_TO_WS_OKHTTP3(handler); // NOLINT(performance-no-int-to-ptr)
|
||||
auto closeReason = JniHelper::jstring2string(reason);
|
||||
RUN_IN_GAMETHREAD(wsOkHttp3->onClose(static_cast<int>(code), closeReason, true));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
JNI_PATH(nativeOnError)(JNIEnv * /*env*/,
|
||||
jobject /*ctx*/,
|
||||
jstring reason,
|
||||
jlong /*identifier*/,
|
||||
jlong handler) {
|
||||
auto *wsOkHttp3 = HANDLE_TO_WS_OKHTTP3(handler); // NOLINT(performance-no-int-to-ptr)
|
||||
int unknownError = static_cast<int>(cc::network::WebSocket::ErrorCode::UNKNOWN);
|
||||
auto errorReason = JniHelper::jstring2string(reason);
|
||||
RUN_IN_GAMETHREAD(wsOkHttp3->onError(unknownError, errorReason));
|
||||
}
|
||||
|
||||
#undef JNI_PATH
|
||||
}
|
||||
|
||||
#undef HANDLE_TO_WS_OKHTTP3
|
||||
251
cocos/network/WebSocket.h
Normal file
251
cocos/network/WebSocket.h
Normal file
@@ -0,0 +1,251 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2010-2012 cocos2d-x.org
|
||||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "base/Macros.h"
|
||||
#include "base/RefCounted.h"
|
||||
#include "base/std/container/vector.h"
|
||||
#include "platform/StdC.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "base/std/container/string.h"
|
||||
#ifndef OBJC_CLASS
|
||||
#ifdef __OBJC__
|
||||
#define OBJC_CLASS(name) @class name
|
||||
#else
|
||||
#define OBJC_CLASS(name) class name
|
||||
#endif
|
||||
#endif // OBJC_CLASS
|
||||
|
||||
OBJC_CLASS(WebSocketImpl);
|
||||
|
||||
/**
|
||||
* @addtogroup network
|
||||
* @{
|
||||
*/
|
||||
|
||||
namespace cc {
|
||||
|
||||
namespace network {
|
||||
|
||||
/**
|
||||
* WebSocket is wrapper of the libwebsockets-protocol, let the develop could call the websocket easily.
|
||||
* Please note that all public methods of WebSocket have to be invoked on Cocos Thread.
|
||||
*/
|
||||
class CC_DLL WebSocket : public RefCounted {
|
||||
public:
|
||||
/**
|
||||
* Close all connections and wait for all websocket threads to exit
|
||||
* @note This method has to be invoked on Cocos Thread
|
||||
*/
|
||||
static void closeAllConnections();
|
||||
|
||||
/**
|
||||
* Constructor of WebSocket.
|
||||
*
|
||||
* @js ctor
|
||||
*/
|
||||
WebSocket();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Destructor of WebSocket.
|
||||
*
|
||||
* @js NA
|
||||
* @lua NA
|
||||
*/
|
||||
~WebSocket() override;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Data structure for message
|
||||
*/
|
||||
struct Data {
|
||||
Data() = default;
|
||||
char *bytes{nullptr};
|
||||
uint32_t len{0}, issued{0};
|
||||
bool isBinary{false};
|
||||
void *ext{nullptr};
|
||||
uint32_t getRemain() const { return std::max(static_cast<uint32_t>(0), len - issued); }
|
||||
};
|
||||
|
||||
/**
|
||||
* ErrorCode enum used to represent the error in the websocket.
|
||||
*/
|
||||
enum class ErrorCode {
|
||||
TIME_OUT, /** < value 0 */
|
||||
CONNECTION_FAILURE, /** < value 1 */
|
||||
UNKNOWN, /** < value 2 */
|
||||
};
|
||||
|
||||
/**
|
||||
* State enum used to represent the Websocket state.
|
||||
*/
|
||||
enum class State {
|
||||
CONNECTING, /** < value 0 */
|
||||
OPEN, /** < value 1 */
|
||||
CLOSING, /** < value 2 */
|
||||
CLOSED, /** < value 3 */
|
||||
};
|
||||
|
||||
/**
|
||||
* The delegate class is used to process websocket events.
|
||||
*
|
||||
* The most member function are pure virtual functions,they should be implemented the in subclass.
|
||||
* @lua NA
|
||||
*/
|
||||
class Delegate {
|
||||
public:
|
||||
/** Destructor of Delegate. */
|
||||
virtual ~Delegate() = default;
|
||||
/**
|
||||
* This function to be called after the client connection complete a handshake with the remote server.
|
||||
* This means that the WebSocket connection is ready to send and receive data.
|
||||
*
|
||||
* @param ws The WebSocket object connected
|
||||
*/
|
||||
virtual void onOpen(WebSocket *ws) = 0;
|
||||
/**
|
||||
* This function to be called when data has appeared from the server for the client connection.
|
||||
*
|
||||
* @param ws The WebSocket object connected.
|
||||
* @param data Data object for message.
|
||||
*/
|
||||
virtual void onMessage(WebSocket *ws, const Data &data) = 0;
|
||||
/**
|
||||
* When the WebSocket object connected wants to close or the protocol won't get used at all and current _readyState is State::CLOSING,this function is to be called.
|
||||
*
|
||||
* @param ws The WebSocket object connected.
|
||||
*/
|
||||
virtual void onClose(WebSocket *ws, uint16_t code, const ccstd::string &reason, bool wasClean) = 0;
|
||||
/**
|
||||
* This function is to be called in the following cases:
|
||||
* 1. client connection is failed.
|
||||
* 2. the request client connection has been unable to complete a handshake with the remote server.
|
||||
* 3. the protocol won't get used at all after this callback and current _readyState is State::CONNECTING.
|
||||
* 4. when a socket descriptor needs to be removed from an external polling array. in is again the struct libwebsocket_pollargs containing the fd member to be removed. If you are using the internal polling loop, you can just ignore it and current _readyState is State::CONNECTING.
|
||||
*
|
||||
* @param ws The WebSocket object connected.
|
||||
* @param error WebSocket::ErrorCode enum,would be ErrorCode::TIME_OUT or ErrorCode::CONNECTION_FAILURE.
|
||||
*/
|
||||
virtual void onError(WebSocket *ws, const ErrorCode &error) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The initialized method for websocket.
|
||||
* It needs to be invoked right after websocket instance is allocated.
|
||||
* @param delegate The delegate which want to receive event from websocket.
|
||||
* @param url The URL of websocket server.
|
||||
* @param protocols The websocket protocols that agree with websocket server
|
||||
* @param caFilePath The ca file path for wss connection
|
||||
* @return true: Success, false: Failure.
|
||||
* @lua NA
|
||||
*/
|
||||
bool init(const Delegate &delegate,
|
||||
const ccstd::string &url,
|
||||
const ccstd::vector<ccstd::string> *protocols = nullptr,
|
||||
const ccstd::string &caFilePath = "");
|
||||
|
||||
/**
|
||||
* @brief Sends string data to websocket server.
|
||||
*
|
||||
* @param message string data.
|
||||
* @lua sendstring
|
||||
*/
|
||||
void send(const ccstd::string &message);
|
||||
|
||||
/**
|
||||
* @brief Sends binary data to websocket server.
|
||||
*
|
||||
* @param binaryMsg binary string data.
|
||||
* @param len the size of binary string data.
|
||||
* @lua sendstring
|
||||
*/
|
||||
void send(const unsigned char *binaryMsg, unsigned int len);
|
||||
|
||||
/**
|
||||
* @brief Closes the connection to server synchronously.
|
||||
* @note It's a synchronous method, it will not return until websocket thread exits.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* @brief Closes the connection to server asynchronously.
|
||||
* @note It's an asynchronous method, it just notifies websocket thread to exit and returns directly,
|
||||
* If using 'closeAsync' to close websocket connection,
|
||||
* be careful of not using destructed variables in the callback of 'onClose'.
|
||||
*/
|
||||
void closeAsync();
|
||||
|
||||
/**
|
||||
* @brief Closes the connection to server asynchronously.
|
||||
* @note It's an asynchronous method, it just notifies websocket thread to exit and returns directly,
|
||||
* If using 'closeAsync' to close websocket connection,
|
||||
* be careful of not using destructed variables in the callback of 'onClose'.
|
||||
* @param code close reason
|
||||
* @param reason reason text description
|
||||
*/
|
||||
void closeAsync(int code, const ccstd::string &reason);
|
||||
|
||||
/**
|
||||
* @brief Gets current state of connection.
|
||||
* @return State the state value could be State::CONNECTING, State::OPEN, State::CLOSING or State::CLOSED
|
||||
*/
|
||||
State getReadyState() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the URL of websocket connection.
|
||||
*/
|
||||
const ccstd::string &getUrl() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the number of bytes of data that have been queued using calls to send() but not yet transmitted to the network.
|
||||
*/
|
||||
size_t getBufferedAmount() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the extensions selected by the server.
|
||||
*/
|
||||
ccstd::string getExtensions() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the protocol selected by websocket server.
|
||||
*/
|
||||
const ccstd::string &getProtocol() const;
|
||||
|
||||
Delegate *getDelegate() const;
|
||||
|
||||
private:
|
||||
WebSocketImpl *_impl{nullptr};
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
} // namespace cc
|
||||
|
||||
// end group
|
||||
/// @}
|
||||
770
cocos/network/WebSocketServer.cpp
Normal file
770
cocos/network/WebSocketServer.cpp
Normal file
@@ -0,0 +1,770 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2019-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Log.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "cocos/network/WebSocketServer.h"
|
||||
|
||||
#define MAX_MSG_PAYLOAD 2048
|
||||
#define SEND_BUFF 1024
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic_int32_t aliveServer{0}; //debug info
|
||||
|
||||
struct lws_protocols protocols[] = {
|
||||
{"", //protocol name
|
||||
cc::network::WebSocketServer::websocketServerCallback,
|
||||
sizeof(int),
|
||||
MAX_MSG_PAYLOAD},
|
||||
{nullptr, nullptr, 0}};
|
||||
|
||||
const struct lws_extension EXTS[] = {
|
||||
{"permessage-deflate",
|
||||
lws_extension_callback_pm_deflate,
|
||||
"permessage-deflate; client_on_context_takeover; client_max_window_bits"},
|
||||
{"deflate-frame",
|
||||
lws_extension_callback_pm_deflate,
|
||||
"deflate-frame"},
|
||||
{nullptr, nullptr, nullptr}};
|
||||
|
||||
struct AsyncTaskData {
|
||||
std::mutex mtx;
|
||||
ccstd::list<std::function<void()>> tasks;
|
||||
};
|
||||
|
||||
// run in server thread loop
|
||||
void flushTasksInServerLoopCb(uv_async_t *async) {
|
||||
auto *data = static_cast<AsyncTaskData *>(async->data);
|
||||
std::lock_guard<std::mutex> guard(data->mtx);
|
||||
while (!data->tasks.empty()) {
|
||||
// fetch task, run task
|
||||
data->tasks.front()();
|
||||
// drop task
|
||||
data->tasks.pop_front();
|
||||
}
|
||||
}
|
||||
void initLibuvAsyncHandle(uv_loop_t *loop, uv_async_t *async) {
|
||||
memset(async, 0, sizeof(uv_async_t));
|
||||
uv_async_init(loop, async, flushTasksInServerLoopCb);
|
||||
async->data = ccnew AsyncTaskData();
|
||||
}
|
||||
|
||||
// run in game thread, dispatch runnable object into server loop
|
||||
void schedule_task_into_server_thread_task_queue(uv_async_t *async, std::function<void()> func) {
|
||||
auto *data = static_cast<AsyncTaskData *>(async->data);
|
||||
if (data) {
|
||||
std::lock_guard<std::mutex> guard(data->mtx);
|
||||
data->tasks.emplace_back(func);
|
||||
}
|
||||
//notify server thread to invoke `flushTasksInServerLoopCb()`
|
||||
uv_async_send(async);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
#define RUN_IN_GAMETHREAD(task) \
|
||||
do { \
|
||||
if (CC_CURRENT_APPLICATION()) { \
|
||||
CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=]() { \
|
||||
task; \
|
||||
}); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define DISPATCH_CALLBACK_IN_GAMETHREAD() \
|
||||
do { \
|
||||
data->setCallback([callback](const ccstd::string &msg) { \
|
||||
auto wrapper = [callback, msg]() { callback(msg); }; \
|
||||
RUN_IN_GAMETHREAD(wrapper()); \
|
||||
}); \
|
||||
} while (0)
|
||||
|
||||
#define RUN_IN_SERVERTHREAD(task) \
|
||||
do { \
|
||||
schedule_task_into_server_thread_task_queue(&_async, [=]() { \
|
||||
task; \
|
||||
}); \
|
||||
} while (0)
|
||||
|
||||
//#define LOGE() CCLOG("WSS: %s", __FUNCTION__)
|
||||
#define LOGE()
|
||||
|
||||
DataFrame::DataFrame(const ccstd::string &data) {
|
||||
_underlyingData.resize(data.size() + LWS_PRE);
|
||||
memcpy(getData(), data.c_str(), data.length());
|
||||
}
|
||||
|
||||
DataFrame::DataFrame(const void *data, int len, bool isBinary) : _isBinary(isBinary) {
|
||||
_underlyingData.resize(len + LWS_PRE);
|
||||
memcpy(getData(), data, len);
|
||||
}
|
||||
|
||||
void DataFrame::append(unsigned char *p, int len) {
|
||||
_underlyingData.insert(_underlyingData.end(), p, p + len);
|
||||
}
|
||||
|
||||
int DataFrame::slice(unsigned char **p, int len) {
|
||||
*p = getData() + _consumed;
|
||||
if (_consumed + len > size()) {
|
||||
return size() - _consumed;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
int DataFrame::consume(int len) {
|
||||
_consumed = (len + _consumed) > size() ? size() : (len + _consumed);
|
||||
return _consumed;
|
||||
}
|
||||
|
||||
int DataFrame::remain() const {
|
||||
return size() - _consumed;
|
||||
}
|
||||
|
||||
ccstd::string DataFrame::toString() {
|
||||
return ccstd::string(reinterpret_cast<char *>(getData()), size());
|
||||
}
|
||||
|
||||
WebSocketServer::WebSocketServer() {
|
||||
aliveServer.fetch_add(1);
|
||||
}
|
||||
|
||||
WebSocketServer::~WebSocketServer() {
|
||||
aliveServer.fetch_sub(1);
|
||||
destroyContext();
|
||||
}
|
||||
|
||||
bool WebSocketServer::close(const std::function<void(const ccstd::string &errorMsg)> &callback) {
|
||||
if (_serverState.load() != ServerThreadState::RUNNING) {
|
||||
return false;
|
||||
}
|
||||
_serverState.store(ServerThreadState::STOPPED);
|
||||
_onclose_cb = callback;
|
||||
if (_ctx) {
|
||||
lws_libuv_stop(_ctx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketServer::closeAsync(const std::function<void(const ccstd::string &errorMsg)> &callback) {
|
||||
if (_serverState.load() != ServerThreadState::RUNNING) {
|
||||
return;
|
||||
}
|
||||
RUN_IN_SERVERTHREAD(this->close(callback));
|
||||
}
|
||||
|
||||
void WebSocketServer::listen(const std::shared_ptr<WebSocketServer> &server, int port, const ccstd::string &host, const std::function<void(const ccstd::string &errorMsg)> &callback) {
|
||||
auto tryLock = server->_serverLock.try_lock();
|
||||
if (!tryLock) {
|
||||
CC_LOG_WARNING("websocketserver is already running!");
|
||||
if (callback) {
|
||||
RUN_IN_GAMETHREAD(callback("Error: Server is already running!"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
server->_serverState = ServerThreadState::RUNNING;
|
||||
//lws_set_log_level(-1, nullptr);
|
||||
|
||||
if (server->_ctx) {
|
||||
server->destroyContext();
|
||||
if (callback) {
|
||||
RUN_IN_GAMETHREAD(callback("Error: lws_context already created!"));
|
||||
}
|
||||
RUN_IN_GAMETHREAD(if (server->_onerror) server->_onerror("websocket listen error!"));
|
||||
server->_serverState = ServerThreadState::ST_ERROR;
|
||||
server->_serverLock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
server->_host = host;
|
||||
|
||||
struct lws_context_creation_info info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.port = port;
|
||||
info.iface = server->_host.empty() ? nullptr : server->_host.c_str();
|
||||
info.protocols = protocols;
|
||||
info.gid = -1;
|
||||
info.uid = -1;
|
||||
info.extensions = EXTS;
|
||||
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_SKIP_SERVER_CANONICAL_NAME;
|
||||
info.timeout_secs = 60;
|
||||
info.max_http_header_pool = 1;
|
||||
info.user = server.get();
|
||||
|
||||
server->_ctx = lws_create_context(&info);
|
||||
|
||||
if (!server->_ctx) {
|
||||
if (callback) {
|
||||
RUN_IN_GAMETHREAD(callback("Error: Failed to create lws_context!"));
|
||||
}
|
||||
RUN_IN_GAMETHREAD(if (server->_onerror) server->_onerror("websocket listen error!"));
|
||||
server->_serverState = ServerThreadState::ST_ERROR;
|
||||
server->_serverLock.unlock();
|
||||
return;
|
||||
}
|
||||
uv_loop_t *loop = nullptr;
|
||||
if (lws_uv_initloop(server->_ctx, loop, 0)) {
|
||||
if (callback) {
|
||||
RUN_IN_GAMETHREAD(callback("Error: Failed to create libuv loop!"));
|
||||
}
|
||||
RUN_IN_GAMETHREAD(if (server->_onerror) server->_onerror("websocket listen error, failed to create libuv loop!"));
|
||||
server->_serverState = ServerThreadState::ST_ERROR;
|
||||
server->_serverLock.unlock();
|
||||
server->destroyContext();
|
||||
return;
|
||||
}
|
||||
|
||||
loop = lws_uv_getloop(server->_ctx, 0);
|
||||
initLibuvAsyncHandle(loop, &server->_async);
|
||||
RUN_IN_GAMETHREAD(if (server->_onlistening) server->_onlistening(""));
|
||||
RUN_IN_GAMETHREAD(if (server->_onbegin) server->_onbegin());
|
||||
RUN_IN_GAMETHREAD(if (callback) callback(""));
|
||||
|
||||
lws_libuv_run(server->_ctx, 0);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&server->_async), nullptr);
|
||||
|
||||
RUN_IN_GAMETHREAD(if (server->_onclose) server->_onclose(""));
|
||||
RUN_IN_GAMETHREAD(if (server->_onclose_cb) server->_onclose_cb(""));
|
||||
RUN_IN_GAMETHREAD(if (server->_onend) server->_onend());
|
||||
server->_serverState = ServerThreadState::STOPPED;
|
||||
server->destroyContext();
|
||||
server->_serverLock.unlock();
|
||||
}
|
||||
|
||||
void WebSocketServer::listenAsync(std::shared_ptr<WebSocketServer> &server, int port, const ccstd::string &host, const std::function<void(const ccstd::string &errorMsg)> &callback) {
|
||||
std::thread([=]() {
|
||||
WebSocketServer::listen(server, port, host, callback);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
ccstd::vector<std::shared_ptr<WebSocketServerConnection>> WebSocketServer::getConnections() const {
|
||||
std::lock_guard<std::mutex> guard(_connsMtx);
|
||||
ccstd::vector<std::shared_ptr<WebSocketServerConnection>> ret;
|
||||
for (auto itr : _conns) {
|
||||
ret.emplace_back(itr.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void WebSocketServer::onCreateClient(struct lws *wsi) {
|
||||
LOGE();
|
||||
std::shared_ptr<WebSocketServerConnection> conn = std::make_shared<WebSocketServerConnection>(wsi);
|
||||
//char ip[221] = { 0 };
|
||||
//char addr[221] = { 0 };
|
||||
//lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), ip, 220, addr, 220);
|
||||
//lws_get_peer_simple(wsi, ip, 220);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(_connsMtx);
|
||||
_conns.emplace(wsi, conn);
|
||||
}
|
||||
RUN_IN_GAMETHREAD(if (_onconnection) _onconnection(conn));
|
||||
conn->onConnected();
|
||||
}
|
||||
|
||||
void WebSocketServer::onDestroyClient(struct lws *wsi) {
|
||||
LOGE();
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
if (conn) {
|
||||
conn->onDestroyClient();
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(_connsMtx);
|
||||
_conns.erase(wsi);
|
||||
}
|
||||
void WebSocketServer::onCloseClient(struct lws *wsi) {
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
if (conn) {
|
||||
conn->onClientCloseInit();
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::onCloseClientInit(struct lws *wsi, void *in, int len) {
|
||||
int16_t code;
|
||||
char *msg = nullptr;
|
||||
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
|
||||
if (conn && len > 2) {
|
||||
code = ntohs(*(int16_t *)in);
|
||||
msg = static_cast<char *>(in) + sizeof(code);
|
||||
ccstd::string cp(msg, len - sizeof(code));
|
||||
conn->onClientCloseInit(code, cp);
|
||||
} else {
|
||||
conn->onClientCloseInit(LWS_CLOSE_STATUS_NORMAL, "Normal");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::onClientReceive(struct lws *wsi, void *in, int len) {
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
if (conn) {
|
||||
conn->onMessageReceive(in, len);
|
||||
}
|
||||
}
|
||||
int WebSocketServer::onServerWritable(struct lws *wsi) {
|
||||
LOGE();
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
if (conn) {
|
||||
return conn->onDrainMessage();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WebSocketServer::onClientHTTP(struct lws *wsi) {
|
||||
LOGE();
|
||||
std::shared_ptr<WebSocketServerConnection> conn = findConnection(wsi);
|
||||
if (conn) {
|
||||
conn->onHTTP();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<WebSocketServerConnection> WebSocketServer::findConnection(struct lws *wsi) {
|
||||
std::shared_ptr<WebSocketServerConnection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(_connsMtx);
|
||||
auto itr = _conns.find(wsi);
|
||||
if (itr != _conns.end()) {
|
||||
conn = itr->second;
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
void WebSocketServer::destroyContext() {
|
||||
_serverState.store(ServerThreadState::DESTROYED);
|
||||
if (_ctx) {
|
||||
lws_context_destroy(_ctx);
|
||||
lws_context_destroy2(_ctx);
|
||||
_ctx = nullptr;
|
||||
}
|
||||
if (_async.data) {
|
||||
delete static_cast<AsyncTaskData *>(_async.data);
|
||||
_async.data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketServerConnection::WebSocketServerConnection(struct lws *wsi) : _wsi(wsi) {
|
||||
uv_loop_t *loop = lws_uv_getloop(lws_get_context(wsi), 0);
|
||||
initLibuvAsyncHandle(loop, &_async);
|
||||
}
|
||||
|
||||
WebSocketServerConnection::~WebSocketServerConnection() {
|
||||
if (_async.data) {
|
||||
delete static_cast<AsyncTaskData *>(_async.data);
|
||||
_async.data = nullptr;
|
||||
}
|
||||
CC_LOG_INFO("~destroy ws connection");
|
||||
}
|
||||
|
||||
bool WebSocketServerConnection::send(std::shared_ptr<DataFrame> data) {
|
||||
_sendQueue.emplace_back(data);
|
||||
if (!_wsi || _closed || _readyState == ReadyState::CLOSING) {
|
||||
return false;
|
||||
}
|
||||
lws_callback_on_writable(_wsi);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::sendTextAsync(const ccstd::string &text, const std::function<void(const ccstd::string &)> &callback) {
|
||||
LOGE();
|
||||
std::shared_ptr<DataFrame> data = std::make_shared<DataFrame>(text);
|
||||
if (callback) {
|
||||
DISPATCH_CALLBACK_IN_GAMETHREAD();
|
||||
}
|
||||
RUN_IN_SERVERTHREAD(this->send(data));
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::sendBinaryAsync(const void *in, size_t len, const std::function<void(const ccstd::string &)> &callback) {
|
||||
LOGE();
|
||||
std::shared_ptr<DataFrame> data = std::make_shared<DataFrame>(in, len);
|
||||
if (callback) {
|
||||
DISPATCH_CALLBACK_IN_GAMETHREAD();
|
||||
}
|
||||
RUN_IN_SERVERTHREAD(this->send(data));
|
||||
}
|
||||
|
||||
bool WebSocketServerConnection::close(int code, const ccstd::string &reason) {
|
||||
if (!_wsi) return false;
|
||||
_readyState = ReadyState::CLOSING;
|
||||
_closeReason = reason;
|
||||
_closeCode = code;
|
||||
onClientCloseInit();
|
||||
//trigger callback to return -1 which indicates connection closed
|
||||
lws_callback_on_writable(_wsi);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::closeAsync(int code, const ccstd::string &reason) {
|
||||
RUN_IN_SERVERTHREAD(this->close(code, reason));
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onConnected() {
|
||||
_readyState = ReadyState::OPEN;
|
||||
RUN_IN_GAMETHREAD(if (_onconnect) _onconnect());
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onMessageReceive(void *in, int len) {
|
||||
bool isFinal = static_cast<bool>(lws_is_final_fragment(_wsi));
|
||||
bool isBinary = static_cast<bool>(lws_frame_is_binary(_wsi));
|
||||
|
||||
if (!_prevPkg) {
|
||||
_prevPkg = std::make_shared<DataFrame>(in, len, isBinary);
|
||||
} else {
|
||||
_prevPkg->append(static_cast<unsigned char *>(in), len);
|
||||
}
|
||||
|
||||
if (isFinal) {
|
||||
//trigger event
|
||||
std::shared_ptr<DataFrame> fullpkg = _prevPkg;
|
||||
if (isBinary) {
|
||||
RUN_IN_GAMETHREAD(if (_onbinary) _onbinary(fullpkg));
|
||||
}
|
||||
if (!isBinary) {
|
||||
RUN_IN_GAMETHREAD(if (_ontext) _ontext(fullpkg));
|
||||
}
|
||||
|
||||
RUN_IN_GAMETHREAD(if (_onmessage) _onmessage(fullpkg));
|
||||
|
||||
_prevPkg.reset();
|
||||
}
|
||||
}
|
||||
|
||||
int WebSocketServerConnection::onDrainMessage() {
|
||||
if (!_wsi) return -1;
|
||||
if (_closed) return -1;
|
||||
if (_readyState == ReadyState::CLOSING) {
|
||||
return -1;
|
||||
}
|
||||
if (_readyState != ReadyState::OPEN) return 0;
|
||||
unsigned char *p = nullptr;
|
||||
int sendLength = 0;
|
||||
int finishLength = 0;
|
||||
int flags = 0;
|
||||
|
||||
ccstd::vector<char> buff(SEND_BUFF + LWS_PRE);
|
||||
|
||||
if (!_sendQueue.empty()) {
|
||||
std::shared_ptr<DataFrame> frag = _sendQueue.front();
|
||||
|
||||
sendLength = frag->slice(&p, SEND_BUFF);
|
||||
|
||||
if (frag->isFront()) {
|
||||
if (frag->isBinary()) {
|
||||
flags |= LWS_WRITE_BINARY;
|
||||
}
|
||||
if (frag->isString()) {
|
||||
flags |= LWS_WRITE_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
if (frag->remain() != sendLength) {
|
||||
// remain bytes > 0
|
||||
// not FIN
|
||||
flags |= LWS_WRITE_NO_FIN;
|
||||
}
|
||||
|
||||
if (!frag->isFront()) {
|
||||
flags |= LWS_WRITE_CONTINUATION;
|
||||
}
|
||||
|
||||
finishLength = lws_write(_wsi, p, sendLength, static_cast<lws_write_protocol>(flags));
|
||||
|
||||
if (finishLength == 0) {
|
||||
frag->onFinish("Connection Closed");
|
||||
return -1;
|
||||
}
|
||||
if (finishLength < 0) {
|
||||
frag->onFinish("Send Error!");
|
||||
return -1;
|
||||
}
|
||||
frag->consume(finishLength);
|
||||
|
||||
if (frag->remain() == 0) {
|
||||
frag->onFinish("");
|
||||
_sendQueue.pop_front();
|
||||
}
|
||||
lws_callback_on_writable(_wsi);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onHTTP() {
|
||||
if (!_wsi) return;
|
||||
|
||||
_headers.clear();
|
||||
|
||||
int n = 0;
|
||||
int len;
|
||||
ccstd::vector<char> buf(256);
|
||||
const char *c;
|
||||
do {
|
||||
auto idx = static_cast<lws_token_indexes>(n);
|
||||
c = reinterpret_cast<const char *>(lws_token_to_string(idx));
|
||||
if (!c) {
|
||||
n++;
|
||||
break;
|
||||
}
|
||||
len = lws_hdr_total_length(_wsi, idx);
|
||||
if (!len) {
|
||||
n++;
|
||||
continue;
|
||||
}
|
||||
if (len + 1 > buf.size()) {
|
||||
buf.resize(len + 1);
|
||||
}
|
||||
lws_hdr_copy(_wsi, buf.data(), static_cast<int>(buf.size()), idx);
|
||||
buf[len] = '\0';
|
||||
_headers.emplace(ccstd::string(c), ccstd::string(buf.data()));
|
||||
n++;
|
||||
} while (c);
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onClientCloseInit(int code, const ccstd::string &msg) {
|
||||
_closeCode = code;
|
||||
_closeReason = msg;
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onClientCloseInit() {
|
||||
if (_closed) return;
|
||||
lws_close_reason(_wsi, static_cast<lws_close_status>(_closeCode), const_cast<unsigned char *>(reinterpret_cast<const unsigned char *>(_closeReason.c_str())), _closeReason.length());
|
||||
_closed = true;
|
||||
}
|
||||
|
||||
void WebSocketServerConnection::onDestroyClient() {
|
||||
_readyState = ReadyState::CLOSED;
|
||||
//on wsi destroyed
|
||||
if (_wsi) {
|
||||
RUN_IN_GAMETHREAD(if (_onclose) _onclose(_closeCode, _closeReason));
|
||||
RUN_IN_GAMETHREAD(if (_onend) _onend());
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&_async), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
ccstd::vector<ccstd::string> WebSocketServerConnection::getProtocols() {
|
||||
ccstd::vector<ccstd::string> ret;
|
||||
if (_wsi) {
|
||||
//TODO(): cause abort
|
||||
//const struct lws_protocols* protos = lws_get_protocol(_wsi);
|
||||
//while (protos && protos->name != nullptr)
|
||||
//{
|
||||
// ret.emplace_back(protos->name);
|
||||
// protos++;
|
||||
//}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ccstd::unordered_map<ccstd::string, ccstd::string> WebSocketServerConnection::getHeaders() {
|
||||
if (!_wsi) return {};
|
||||
return _headers;
|
||||
}
|
||||
|
||||
int WebSocketServer::websocketServerCallback(struct lws *wsi, enum lws_callback_reasons reason,
|
||||
void * /*user*/, void *in, size_t len) {
|
||||
int ret = 0;
|
||||
WebSocketServer *server = nullptr;
|
||||
lws_context *ctx = nullptr;
|
||||
|
||||
if (wsi) {
|
||||
ctx = lws_get_context(wsi);
|
||||
}
|
||||
if (ctx) {
|
||||
server = static_cast<WebSocketServer *>(lws_context_user(ctx));
|
||||
}
|
||||
|
||||
if (!server) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_ESTABLISHED:
|
||||
break;
|
||||
case LWS_CALLBACK_CLOSED:
|
||||
break;
|
||||
case LWS_CALLBACK_CLOSED_HTTP:
|
||||
break;
|
||||
case LWS_CALLBACK_RECEIVE:
|
||||
server->onClientReceive(wsi, in, static_cast<int>(len));
|
||||
break;
|
||||
case LWS_CALLBACK_RECEIVE_PONG:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_RECEIVE:
|
||||
server->onClientReceive(wsi, in, static_cast<int>(len));
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_RECEIVE_PONG:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_WRITEABLE:
|
||||
//ret = server->onClientWritable(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_SERVER_WRITEABLE:
|
||||
ret = server->onServerWritable(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_BODY:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_BODY_COMPLETION:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_FILE_COMPLETION:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_WRITEABLE:
|
||||
break;
|
||||
case LWS_CALLBACK_FILTER_NETWORK_CONNECTION:
|
||||
break;
|
||||
case LWS_CALLBACK_FILTER_HTTP_CONNECTION:
|
||||
break;
|
||||
case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED:
|
||||
break;
|
||||
case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
|
||||
break;
|
||||
case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
|
||||
break;
|
||||
case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS:
|
||||
break;
|
||||
case LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
|
||||
break;
|
||||
case LWS_CALLBACK_CONFIRM_EXTENSION_OKAY:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED:
|
||||
break;
|
||||
case LWS_CALLBACK_PROTOCOL_INIT:
|
||||
break;
|
||||
case LWS_CALLBACK_PROTOCOL_DESTROY:
|
||||
break;
|
||||
case LWS_CALLBACK_WSI_CREATE:
|
||||
server->onCreateClient(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_WSI_DESTROY:
|
||||
server->onDestroyClient(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_GET_THREAD_ID:
|
||||
break;
|
||||
case LWS_CALLBACK_ADD_POLL_FD:
|
||||
break;
|
||||
case LWS_CALLBACK_DEL_POLL_FD:
|
||||
break;
|
||||
case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
|
||||
break;
|
||||
case LWS_CALLBACK_LOCK_POLL:
|
||||
break;
|
||||
case LWS_CALLBACK_UNLOCK_POLL:
|
||||
break;
|
||||
case LWS_CALLBACK_OPENSSL_CONTEXT_REQUIRES_PRIVATE_KEY:
|
||||
break;
|
||||
case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE:
|
||||
server->onCloseClientInit(wsi, in, static_cast<int>(len));
|
||||
break;
|
||||
case LWS_CALLBACK_WS_EXT_DEFAULTS:
|
||||
break;
|
||||
case LWS_CALLBACK_CGI:
|
||||
break;
|
||||
case LWS_CALLBACK_CGI_TERMINATED:
|
||||
break;
|
||||
case LWS_CALLBACK_CGI_STDIN_DATA:
|
||||
break;
|
||||
case LWS_CALLBACK_CGI_STDIN_COMPLETED:
|
||||
break;
|
||||
case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
|
||||
break;
|
||||
case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
|
||||
server->onCloseClient(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
|
||||
break;
|
||||
case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
|
||||
break;
|
||||
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_BIND_PROTOCOL:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_DROP_PROTOCOL:
|
||||
break;
|
||||
case LWS_CALLBACK_CHECK_ACCESS_RIGHTS:
|
||||
break;
|
||||
case LWS_CALLBACK_PROCESS_HTML:
|
||||
break;
|
||||
case LWS_CALLBACK_ADD_HEADERS:
|
||||
server->onClientHTTP(wsi);
|
||||
break;
|
||||
case LWS_CALLBACK_SESSION_INFO:
|
||||
break;
|
||||
case LWS_CALLBACK_GS_EVENT:
|
||||
break;
|
||||
case LWS_CALLBACK_HTTP_PMO:
|
||||
break;
|
||||
case LWS_CALLBACK_CLIENT_HTTP_WRITEABLE:
|
||||
break;
|
||||
case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_RX:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_CLOSE:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_WRITEABLE:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_ADOPT:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_ADOPT_FILE:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_RX_FILE:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_WRITEABLE_FILE:
|
||||
break;
|
||||
case LWS_CALLBACK_RAW_CLOSE_FILE:
|
||||
break;
|
||||
case LWS_CALLBACK_SSL_INFO:
|
||||
break;
|
||||
case LWS_CALLBACK_CHILD_WRITE_VIA_PARENT:
|
||||
break;
|
||||
case LWS_CALLBACK_CHILD_CLOSING:
|
||||
break;
|
||||
case LWS_CALLBACK_CGI_PROCESS_ATTACH:
|
||||
break;
|
||||
case LWS_CALLBACK_USER:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
297
cocos/network/WebSocketServer.h
Normal file
297
cocos/network/WebSocketServer.h
Normal file
@@ -0,0 +1,297 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2019-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
// clang-format on
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "base/Macros.h"
|
||||
#include "base/std/container/list.h"
|
||||
#include "base/std/container/string.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
#include "base/std/container/unordered_map.h"
|
||||
#include "uv.h"
|
||||
|
||||
#if CC_PLATFORM == CC_PLATFORM_OHOS
|
||||
#include "libwebsockets.h"
|
||||
#else
|
||||
#include "websockets/libwebsockets.h"
|
||||
#endif
|
||||
|
||||
namespace cc {
|
||||
namespace network {
|
||||
|
||||
class WebSocketServer;
|
||||
class WebSocketServerConnection;
|
||||
|
||||
/**
|
||||
* receive/send data buffer with reserved bytes
|
||||
*/
|
||||
class DataFrame {
|
||||
public:
|
||||
explicit DataFrame(const ccstd::string &data);
|
||||
|
||||
DataFrame(const void *data, int len, bool isBinary = true);
|
||||
|
||||
virtual ~DataFrame() = default;
|
||||
|
||||
void append(unsigned char *p, int len);
|
||||
|
||||
int slice(unsigned char **p, int len);
|
||||
|
||||
int consume(int len);
|
||||
|
||||
int remain() const;
|
||||
|
||||
inline bool isBinary() const { return _isBinary; }
|
||||
inline bool isString() const { return !_isBinary; }
|
||||
inline bool isFront() const { return _consumed == 0; }
|
||||
|
||||
void setCallback(const std::function<void(const ccstd::string &)> &callback) {
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
void onFinish(const ccstd::string &message) {
|
||||
if (_callback) {
|
||||
_callback(message);
|
||||
}
|
||||
}
|
||||
|
||||
inline int size() const { return static_cast<int>(_underlyingData.size() - LWS_PRE); }
|
||||
|
||||
ccstd::string toString();
|
||||
|
||||
unsigned char *getData() { return _underlyingData.data() + LWS_PRE; }
|
||||
|
||||
private:
|
||||
ccstd::vector<unsigned char> _underlyingData;
|
||||
int _consumed = 0;
|
||||
bool _isBinary = false;
|
||||
std::function<void(const ccstd::string &)> _callback;
|
||||
};
|
||||
|
||||
class CC_DLL WebSocketServerConnection {
|
||||
public:
|
||||
explicit WebSocketServerConnection(struct lws *wsi);
|
||||
virtual ~WebSocketServerConnection();
|
||||
|
||||
enum ReadyState {
|
||||
CONNECTING = 1,
|
||||
OPEN = 2,
|
||||
CLOSING = 3,
|
||||
CLOSED = 4
|
||||
};
|
||||
|
||||
void sendTextAsync(const ccstd::string &, const std::function<void(const ccstd::string &)> &callback);
|
||||
|
||||
void sendBinaryAsync(const void *, size_t len, const std::function<void(const ccstd::string &)> &callback);
|
||||
|
||||
void closeAsync(int code, const ccstd::string &reason);
|
||||
|
||||
/** stream is not implemented*/
|
||||
//bool beginBinary();
|
||||
|
||||
/** should implement in JS */
|
||||
// bool send();
|
||||
// bool sendPing(ccstd::string)
|
||||
|
||||
//int getSocket();
|
||||
//std::shared_ptr<WebSocketServer>& getServer();
|
||||
//ccstd::string& getPath();
|
||||
|
||||
inline int getReadyState() const {
|
||||
return static_cast<int>(_readyState);
|
||||
}
|
||||
|
||||
ccstd::unordered_map<ccstd::string, ccstd::string> getHeaders();
|
||||
|
||||
ccstd::vector<ccstd::string> getProtocols();
|
||||
|
||||
inline void setOnClose(const std::function<void(int, const ccstd::string &)> &cb) {
|
||||
_onclose = cb;
|
||||
}
|
||||
|
||||
inline void setOnError(const std::function<void(const ccstd::string &)> &cb) {
|
||||
_onerror = cb;
|
||||
}
|
||||
|
||||
inline void setOnText(const std::function<void(std::shared_ptr<DataFrame>)> &cb) {
|
||||
_ontext = cb;
|
||||
}
|
||||
|
||||
inline void setOnBinary(const std::function<void(std::shared_ptr<DataFrame>)> &cb) {
|
||||
_onbinary = cb;
|
||||
}
|
||||
|
||||
inline void setOnMessage(const std::function<void(std::shared_ptr<DataFrame>)> &cb) {
|
||||
_onmessage = cb;
|
||||
}
|
||||
|
||||
inline void setOnConnect(const std::function<void()> &cb) {
|
||||
_onconnect = cb;
|
||||
}
|
||||
|
||||
inline void setOnEnd(const std::function<void()> &cb) {
|
||||
_onend = cb;
|
||||
}
|
||||
|
||||
void onClientCloseInit();
|
||||
|
||||
inline void setData(void *data) { _data = data; }
|
||||
inline void *getData() const { return _data; }
|
||||
|
||||
private:
|
||||
bool send(std::shared_ptr<DataFrame> data);
|
||||
bool close(int code, const ccstd::string &reason);
|
||||
|
||||
inline void scheduleSend() {
|
||||
if (_wsi) {
|
||||
lws_callback_on_writable(_wsi);
|
||||
}
|
||||
}
|
||||
|
||||
void onConnected();
|
||||
void onMessageReceive(void *in, int len);
|
||||
int onDrainMessage();
|
||||
void onHTTP();
|
||||
void onClientCloseInit(int code, const ccstd::string &msg);
|
||||
|
||||
void onDestroyClient();
|
||||
|
||||
struct lws *_wsi = nullptr;
|
||||
ccstd::unordered_map<ccstd::string, ccstd::string> _headers;
|
||||
ccstd::list<std::shared_ptr<DataFrame>> _sendQueue;
|
||||
std::shared_ptr<DataFrame> _prevPkg;
|
||||
bool _closed = false;
|
||||
ccstd::string _closeReason = "close connection";
|
||||
int _closeCode = 1000;
|
||||
std::atomic<ReadyState> _readyState{ReadyState::CLOSED};
|
||||
|
||||
// Attention: do not reference **this** in callbacks
|
||||
std::function<void(int, const ccstd::string &)> _onclose;
|
||||
std::function<void(const ccstd::string &)> _onerror;
|
||||
std::function<void(std::shared_ptr<DataFrame>)> _ontext;
|
||||
std::function<void(std::shared_ptr<DataFrame>)> _onbinary;
|
||||
std::function<void(std::shared_ptr<DataFrame>)> _onmessage;
|
||||
std::function<void()> _onconnect;
|
||||
std::function<void()> _onend;
|
||||
uv_async_t _async = {};
|
||||
void *_data = nullptr;
|
||||
|
||||
friend class WebSocketServer;
|
||||
};
|
||||
|
||||
class CC_DLL WebSocketServer {
|
||||
public:
|
||||
WebSocketServer();
|
||||
virtual ~WebSocketServer();
|
||||
|
||||
static void listenAsync(std::shared_ptr<WebSocketServer> &server, int port, const ccstd::string &host, const std::function<void(const ccstd::string &errorMsg)> &callback);
|
||||
void closeAsync(const std::function<void(const ccstd::string &errorMsg)> &callback = nullptr);
|
||||
|
||||
ccstd::vector<std::shared_ptr<WebSocketServerConnection>> getConnections() const;
|
||||
|
||||
void setOnListening(const std::function<void(const ccstd::string &)> &cb) {
|
||||
_onlistening = cb;
|
||||
}
|
||||
|
||||
void setOnError(const std::function<void(const ccstd::string &)> &cb) {
|
||||
_onerror = cb;
|
||||
}
|
||||
|
||||
void setOnClose(const std::function<void(const ccstd::string &)> &cb) {
|
||||
_onclose = cb;
|
||||
}
|
||||
|
||||
void setOnConnection(const std::function<void(std::shared_ptr<WebSocketServerConnection>)> &cb) {
|
||||
_onconnection = cb;
|
||||
}
|
||||
|
||||
inline void setOnEnd(const std::function<void()> &cb) {
|
||||
_onend = cb;
|
||||
}
|
||||
|
||||
inline void setOnBegin(const std::function<void()> &cb) {
|
||||
_onbegin = cb;
|
||||
}
|
||||
|
||||
inline void setData(void *d) { _data = d; }
|
||||
inline void *getData() const { return _data; }
|
||||
|
||||
protected:
|
||||
static void listen(const std::shared_ptr<WebSocketServer> &server, int port, const ccstd::string &host, const std::function<void(const ccstd::string &errorMsg)> &callback);
|
||||
bool close(const std::function<void(const ccstd::string &errorMsg)> &callback = nullptr);
|
||||
|
||||
void onCreateClient(struct lws *wsi);
|
||||
void onDestroyClient(struct lws *wsi);
|
||||
void onCloseClient(struct lws *wsi);
|
||||
void onCloseClientInit(struct lws *wsi, void *in, int len);
|
||||
void onClientReceive(struct lws *wsi, void *in, int len);
|
||||
int onServerWritable(struct lws *wsi);
|
||||
void onClientHTTP(struct lws *wsi);
|
||||
|
||||
private:
|
||||
std::shared_ptr<WebSocketServerConnection> findConnection(struct lws *wsi);
|
||||
void destroyContext();
|
||||
|
||||
ccstd::string _host;
|
||||
lws_context *_ctx = nullptr;
|
||||
uv_async_t _async = {};
|
||||
|
||||
mutable std::mutex _connsMtx;
|
||||
ccstd::unordered_map<struct lws *, std::shared_ptr<WebSocketServerConnection>> _conns;
|
||||
|
||||
// Attention: do not reference **this** in callbacks
|
||||
std::function<void(const ccstd::string &)> _onlistening;
|
||||
std::function<void(const ccstd::string &)> _onerror;
|
||||
std::function<void(const ccstd::string &)> _onclose;
|
||||
std::function<void(const ccstd::string &)> _onclose_cb;
|
||||
std::function<void()> _onend;
|
||||
std::function<void()> _onbegin;
|
||||
std::function<void(std::shared_ptr<WebSocketServerConnection>)> _onconnection;
|
||||
|
||||
enum class ServerThreadState {
|
||||
NOT_BOOTED,
|
||||
ST_ERROR,
|
||||
RUNNING,
|
||||
STOPPED,
|
||||
DESTROYED
|
||||
};
|
||||
std::atomic<ServerThreadState> _serverState{ServerThreadState::NOT_BOOTED};
|
||||
std::mutex _serverLock;
|
||||
void *_data = nullptr;
|
||||
|
||||
public:
|
||||
static int websocketServerCallback(struct lws *wsi, enum lws_callback_reasons reason,
|
||||
void *user, void *in, size_t len);
|
||||
};
|
||||
} // namespace network
|
||||
} // namespace cc
|
||||
Reference in New Issue
Block a user