no message

This commit is contained in:
gem
2025-02-18 15:21:31 +08:00
commit 2d133e56d7
1980 changed files with 465595 additions and 0 deletions

View 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

View 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

View 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" {

View 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

View 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
View 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

View 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

View 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

View 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

View 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__

View 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)

View 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

View 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

View 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
View 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
/// @}

View 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;
}

View 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
View 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__

View 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

File diff suppressed because it is too large Load Diff

261
cocos/network/SocketIO.h Normal file
View 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
View 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
View 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
/// @}

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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, /** &lt; value 0 */
CONNECTION_FAILURE, /** &lt; value 1 */
UNKNOWN, /** &lt; value 2 */
};
/**
* State enum used to represent the Websocket state.
*/
enum class State {
CONNECTING, /** &lt; value 0 */
OPEN, /** &lt; value 1 */
CLOSING, /** &lt; value 2 */
CLOSED, /** &lt; 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
/// @}

View 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

View 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