游戏开发中需要动态下载资源。甚至支持热更新修复代码。都需要引擎支持文件下载功能。写个基于http协议的多线程文件下载器。用第三方库如何选择问题?如何实现接口方便调用问题?等等。其实前面也多次用到libcurl。libcurl作为是一个多协议的便于客户端使用的URL传输库。libcurl已经非常灵活。接口设计的非常细。完全可以满足需求。
这里分享一个基于libcurl的HTTP封装类,其功能包括:同步的(HTTP/HTTPS)GET、POST请求,以及文件下载和进度报告。通过网络通信1-5中通信相关的方式。游戏开发中用到的通信基本都囊括了。引擎底层已经具备了强大的通信能力和提供完善的接口。后续的重点就是如何使用好接口。和提高开发效率。尤其是团队中多人参与研发。如何高效协助的问题。必然需要完整的工具链实现网络消息生成。使真正开发者无需关注底层。只需依葫芦画瓢接收和发送消息即可。但作为游戏开发者还是需要多花些精力去实现下。知识大爆炸时代更需要组织代码能力强的人才。尽量减少代码编程的时间。
C++实现下载器类:Gk8HttpLoader.h
#ifndef __GK8HTTPLOADER_H__
#define __GK8HTTPLOADER_H__
#pragma once
#include "Gk8BaseObj.h"
#include "Gk8ByteMaker.h"
#include "curl/curl.h"
#define GK8LOADER_COUNT 0x0001
#define GK8LOADER_SIZE 0x0002
#define GK8LOADER_ONESIZE 0x0003
class Gk8HttpLoader:public Gk8BaseObj
{
DECLARE_TOSPP_MAP;
private:
GK8_BOOL m_bLoading; //是否正在文件下载
Gk8ByteMaker m_iFileBufData; //[文件BUFF数据]
GK8_INT m_nLoadShowCase; //[下载显示模式:1个数进度,2整体大小进度,3单个大小进度]
Gk8Var m_iLoadProgressCallVar;
Gk8Var m_iLoadFinishCallVar;
GK8_INT m_nLoadFileCount; //[已下载文件个数]
GK8_INT m_nSumFileCount; //[总文件个数]
GK8_INT m_nLoadFileSize; //[已下载文件大小]
GK8_INT m_nSumFileSize; //[总文件大小]
private:
GK8_BOOL InitLoaderThreadSemphore();
public:
Gk8HttpLoader();
virtual ~Gk8HttpLoader();
static Gk8HttpLoader* GetInstance();
GK8_VOID Destroy();
GK8_VOID TOSPPFUNC AddLoaderFile(GK8_LPCSTR lpFileName,GK8_LPCSTR lpLoaderUrl,GK8_LPCSTR lpLocalFile,GK8_INT nFileSize);
GK8_VOID TOSPPFUNC SetHttpLoad(GK8_INT nLoadShowCase,Gk8Var& iLoadProgressCallVar,Gk8Var& iLoadFinishCallVar);
GK8_VOID TOSPPFUNC HttpLoaderFile();
;
GK8_VOID OnProgress(GK8_LPVOID lpLoaderData,GK8_CDOUBLE dlTotal,GK8_CDOUBLE dlNow);
GK8_VOID ShowLoadBar(GK8_LPVOID lpLoaderData);
GK8_VOID HttpLoaderTick();
};
#endif
C++实现下载器类:Gk8HttpLoader.cpp
#include "Gk8HttpLoader.h"
#include "Gk8OperSys.h"
#include "Gk8SetMb.cpp"
#include "Gk8BufEntry.cpp"
#include "Gk8FileUtil.h"
#include <queue>
#include <pthread.h>
#include <semaphore.h>
#include <errno.h>
#include <fcntl.h>
typedef struct tagLOADERDATA
{
GK8_UINT nNameId; //[文件名ID]
GK8_LPCSTR pFileName; //[下载文件名]
GK8_LPCSTR pLoaderUrl; //[下载HTTP URL]
GK8_LPCSTR pLocalFile; //[本地文件存放地]
GK8_INT nFileSize; //[文件大小]
Gk8ByteMaker* pFileBufData; //[文件BUFF数据]
GK8_BOOL bSucceed; //[HTTP下载请求反馈成功]
GK8_INT nResponseCode; //[HTTP下载请求反馈码]
Gk8Str sErrorBuffer; //[HTTP下载请求错误信息]
}LOADERDATA,*LPLOADERDATA;
static Gk8Str sg_iLoadProgressEvent("OnLoadProgress"); //[下载进度信息]
static Gk8Str sg_iLoadFinishEvent("OnLoadFinish"); //[下载完成信息]
static Gk8HttpLoader* sg_pHttpLoader=NULL; //[静态HTTP下载器]
static Gk8BufEntry<LOADERDATA,256>* s_pLoaderCache=NULL;
static GK8_BOOL sg_bHttpLoaderQuit=false; //[退出HTTP下载器]
static sem_t* sg_pLoaderSem=NULL; //[信号量的数据类型]
//[线程及互斥定义]
static pthread_t sg_LoaderNetWorkThread; //[HTTP下载线程]
static pthread_mutex_t sg_RequestLoaderMutex; //[HTTP请求下载互斥体]
static pthread_mutex_t sg_ResponseLoaderMutex; //[HTTP响应下载互斥体]
static Gk8SetMb<LPLOADERDATA> sg_iRequestLoaderQueue; //[HTTP下载请求队列]
static Gk8SetMb<LPLOADERDATA> sg_iResponseLoaderQueue; //[HTTP下载响应队列]
static GK8_CHAR sg_szLoaderErrorBuf[CURL_ERROR_SIZE]; //[错误信息]
static Gk8Str sg_iLoaderErrorStr;
typedef size_t (*HTTPLOADERWRITE_CALLBACK)(GK8_LPVOID lpData,size_t nSize,size_t nMemBlock,GK8_LPVOID lpResponseData);
#if (GK8_TARGET_PLATFORM==GK8_PLATFORM_IOS)
#define GK8_ASYNC_HTTPREQUEST_USE_NAMED_SEMAPHORE 1
#else
#define GK8_ASYNC_HTTPREQUEST_USE_NAMED_SEMAPHORE 0
#endif
#if GK8_ASYNC_HTTPREQUEST_USE_NAMED_SEMAPHORE
#define GK8_ASYNC_HTTPREQUEST_SEMAPHORE "Gk8HttpAsync"
#else
static sem_t sg_iLoaderSem;
#endif
GK8_INT ProcessLoaderTask(LPLOADERDATA pHttpLoaderResponse,HTTPLOADERWRITE_CALLBACK lpCallBack,GK8_LPINT lpResponseCode);
//[接收HTTP下载数据]
size_t WriteHttpLoaderData(GK8_LPVOID lpData,size_t nSize,size_t nMemBlock,GK8_LPVOID lpResponseData)
{
Gk8ByteMaker* pResponseData=(Gk8ByteMaker*)lpResponseData;
size_t nLength=nSize*nMemBlock;
pResponseData->WriteBuf(lpData,(GK8_INT)nLength);
return nLength;
}
//[进度回调]
size_t HandleLoaderProgress(GK8_LPVOID lpBuffer,GK8_DOUBLE dlTotal,GK8_DOUBLE dlNow,GK8_DOUBLE ulTotal,GK8_DOUBLE ulNow)
{
LPLOADERDATA pHttpLoaderResponse=(LPLOADERDATA)lpBuffer;
Gk8HttpLoader* pHttpLoader=Gk8HttpLoader::GetInstance();
pHttpLoader->OnProgress(pHttpLoaderResponse,dlTotal,dlNow);
return 0;
}
//[处理POST请求:流的行事发出]
GK8_INT ProcessLoaderTask(LPLOADERDATA pHttpLoaderResponse,HTTPLOADERWRITE_CALLBACK lpWriteCallBack,GK8_LPINT lpResponseCode)
{
CURLcode nCurlCode=CURL_LAST;
CURL* pCurl=curl_easy_init();
do
{
//远程URL,支持HTTP,HTTPS,FTP
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_URL,pHttpLoaderResponse->pLoaderUrl);
if(nCurlCode!=CURLE_OK) break;
//设置User-Agent
Gk8Str iUserAgentStr="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101 Firefox/13.0.1";
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_USERAGENT,iUserAgentStr);
if(nCurlCode!=CURLE_OK) break;
//设置重定向的最大次数
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_MAXREDIRS,5);
if(nCurlCode!=CURLE_OK) break;
//设置301,302跳转跟随location
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_FOLLOWLOCATION,1);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_NOSIGNAL,1L);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_POST,false);
if(nCurlCode!=CURLE_OK) break;
//下载内容回调函数
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_WRITEFUNCTION,lpWriteCallBack);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_WRITEDATA,pHttpLoaderResponse->pFileBufData);
if(nCurlCode!=CURLE_OK) break;
//进度回调函数
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_NOPROGRESS, 0);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_PROGRESSDATA,pHttpLoaderResponse);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_PROGRESSFUNCTION,HandleLoaderProgress);
if(nCurlCode!=CURLE_OK) break;
//跳过服务器SSL验证,不使用CA证书
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_SSL_VERIFYPEER,0L);
if(nCurlCode!=CURLE_OK) break;
//验证服务器端发送的证书,默认是2(高),1(中),0(禁用)
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_SSL_VERIFYHOST,0L);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_setopt(pCurl,CURLOPT_ERRORBUFFER,sg_szLoaderErrorBuf);
if(nCurlCode!=CURLE_OK) return false;
nCurlCode=curl_easy_perform(pCurl);
if(nCurlCode!=CURLE_OK) break;
nCurlCode=curl_easy_getinfo(pCurl,CURLINFO_RESPONSE_CODE,lpResponseCode);
if(nCurlCode!=CURLE_OK||*lpResponseCode!=200)
{
nCurlCode=CURLE_HTTP_RETURNED_ERROR;
}
}while(0);
if(pCurl) curl_easy_cleanup(pCurl);
return (nCurlCode==CURLE_OK?0:1);
}
//[创建网络下载线程]
static GK8_LPVOID LoaderNetWorkThread(GK8_LPVOID lpData)
{
LPLOADERDATA pHttpLoaderRequest=NULL;
LPLOADERDATA pHttpLoaderResponse=NULL;
while(true)
{
//Wait for http request tasks from main thread
GK8_INT nSemWaitRet=sem_wait(sg_pLoaderSem);
if(nSemWaitRet<0)
{
_GK8ERR<<"HttpRequest async thread semaphore error:"<<strerror(errno)<<CR;
break;
}
if(sg_bHttpLoaderQuit) break;
//[第一步:发送HTTP请求]
pHttpLoaderRequest=NULL;
pthread_mutex_lock(&sg_RequestLoaderMutex);
if(0!=sg_iRequestLoaderQueue.GetSize())
{
pHttpLoaderRequest=sg_iRequestLoaderQueue.GetItemAt(0);
sg_iRequestLoaderQueue.RemoveItemAt(0);
}
pthread_mutex_unlock(&sg_RequestLoaderMutex);
if(NULL==pHttpLoaderRequest) continue;
//[清除二进制数据]
pHttpLoaderRequest->pFileBufData->ClearStream();
//[第二步:libCurl同步请求]
//pHttpLoaderResponse=new LOADERDATA();
//memcpy(pHttpLoaderResponse,pHttpLoaderRequest,sizeof(LOADERDATA));
pHttpLoaderResponse=pHttpLoaderRequest;
GK8_INT nResponseCode=-1;
GK8_INT nRetValue=0;
nRetValue=ProcessLoaderTask(pHttpLoaderResponse,WriteHttpLoaderData,&nResponseCode);
pHttpLoaderResponse->nResponseCode=nResponseCode;
if(nRetValue!=0)
{
pHttpLoaderResponse->bSucceed=false;
sg_iLoaderErrorStr=sg_szLoaderErrorBuf;
pHttpLoaderResponse->sErrorBuffer=sg_iLoaderErrorStr;
}else
{
pHttpLoaderResponse->bSucceed=true;
}
pthread_mutex_lock(&sg_ResponseLoaderMutex);
sg_iResponseLoaderQueue.AddItem(pHttpLoaderResponse);
pthread_mutex_unlock(&sg_ResponseLoaderMutex);
}
pthread_mutex_lock(&sg_RequestLoaderMutex);
sg_iRequestLoaderQueue.Clear();
pthread_mutex_unlock(&sg_RequestLoaderMutex);
if(sg_pLoaderSem!=NULL)
{
#if GK8_ASYNC_HTTPREQUEST_USE_NAMED_SEMAPHORE
sem_unlink(GK8_ASYNC_HTTPREQUEST_SEMAPHORE);
sem_close(sg_pLoaderSem);
#else
sem_destroy(sg_pLoaderSem);
#endif
sg_pLoaderSem=NULL;
pthread_mutex_destroy(&sg_RequestLoaderMutex);
pthread_mutex_destroy(&sg_ResponseLoaderMutex);
//[依次删除数据]
GK8_INT nIndex;
for(nIndex=0;nIndex<sg_iRequestLoaderQueue.GetSize();nIndex++)
{
pHttpLoaderRequest=sg_iRequestLoaderQueue.GetItemAt(nIndex);
delete pHttpLoaderRequest;
}
for(nIndex=0;nIndex<sg_iResponseLoaderQueue.GetSize();nIndex++)
{
pHttpLoaderResponse=sg_iResponseLoaderQueue.GetItemAt(nIndex);
delete pHttpLoaderResponse;
}
sg_iRequestLoaderQueue.Destroy();
sg_iResponseLoaderQueue.Destroy();
}
pthread_exit(NULL);
return 0;
}
/////////////////////////////////////////////CLASS-TOSPP////////////////////////////////////////////////////
BEGIN_TOSPP_MAP(Gk8HttpLoader,Gk8BaseObj)
TOSPP_FUNC(Gk8HttpLoader,AddLoaderFile,' ',"sssd","AddLoaderFile(lpFileName,lpLoaderUrl,lpLocalFile,nFileSize)")
TOSPP_FUNC(Gk8HttpLoader,HttpLoaderFile,' ',"","HttpLoaderFile()")
TOSPP_FUNC(Gk8HttpLoader,SetHttpLoad,' ',"d&v&v","SetHttpLoad(nLoadShowCase,iLoadProgressCallVar,iLoadFinishCallVar)")
END_TOSPP_MAP()
BEGIN_TOSPP_STATIC_FUN_MAP(Gk8HttpLoader)
TOSPP_STATIC_FUNC(Gk8HttpLoader,GetInstance,'p',"","[Gk8HttpLoader* GetInstance()?创建对象]")
END_TOSPP_STATIC_FUN_MAP
/////////////////////////////////////////////////////////////////////////////////////////////////
Gk8HttpLoader::Gk8HttpLoader()
{
m_bLoading=false;
m_nLoadShowCase=1;
m_nLoadFileCount=0;
m_nSumFileCount=0;
m_nLoadFileSize=0;
m_nSumFileSize=0;
s_pLoaderCache=new Gk8BufEntry<LOADERDATA,256>;
}
Gk8HttpLoader::~Gk8HttpLoader()
{
sg_bHttpLoaderQuit=true;
if(sg_pLoaderSem!=NULL)
{
sem_post(sg_pLoaderSem);
}
Destroy();
}
//[初始化线程]
GK8_BOOL Gk8HttpLoader::InitLoaderThreadSemphore()
{
if(sg_pLoaderSem!=NULL)
{
return true;
}else
{
#if GK8_ASYNC_HTTPREQUEST_USE_NAMED_SEMAPHORE
sg_pLoaderSem=sem_open(GK8_ASYNC_HTTPREQUEST_SEMAPHORE,O_CREAT,0644,0);
if(sg_pLoaderSem==SEM_FAILED)
{
_GK8ERR<<"Open HttpLoader Semaphore Failed"<<CR;
sg_pLoaderSem=NULL;
return false;
}
#else
GK8_INT nSemRet=sem_init(&sg_iLoaderSem,0,0);
if(nSemRet<0)
{
_GK8ERR<<"Init HttpLoader Semaphore Failed"<<CR;
return false;
}
sg_pLoaderSem=&sg_iLoaderSem;
#endif
pthread_mutex_init(&sg_RequestLoaderMutex,NULL);
pthread_mutex_init(&sg_ResponseLoaderMutex,NULL);
pthread_create(&sg_LoaderNetWorkThread,NULL,LoaderNetWorkThread,NULL);
pthread_detach(sg_LoaderNetWorkThread);
sg_bHttpLoaderQuit=false;
}
return true;
}
//[增加HTTP下载文件]
GK8_VOID Gk8HttpLoader::AddLoaderFile(GK8_LPCSTR lpFileName,GK8_LPCSTR lpLoaderUrl,GK8_LPCSTR lpLocalFile,GK8_INT nFileSize)
{
GK8_UINT nNameId=Gk8OperSys::Str2UINT(lpFileName);
LPLOADERDATA lpLoaderData=s_pLoaderCache->AddEntryWithNameId(nNameId);
lpLoaderData->nNameId=nNameId;
lpLoaderData->pFileName=lpFileName;
lpLoaderData->pLoaderUrl=lpLoaderUrl;
lpLoaderData->pLocalFile=lpLocalFile;
lpLoaderData->nFileSize=nFileSize;
lpLoaderData->pFileBufData=&m_iFileBufData;
//[准备显示数据]
m_nSumFileCount++;
m_nSumFileSize+=nFileSize;
m_iFileBufData.WriteAlloc(nFileSize);
}
GK8_VOID Gk8HttpLoader::SetHttpLoad(GK8_INT nLoadShowCase,Gk8Var& iLoadProgressCallVar,Gk8Var& iLoadFinishCallVar)
{
m_nLoadShowCase=nLoadShowCase;
m_iLoadProgressCallVar=iLoadProgressCallVar;
m_iLoadFinishCallVar=iLoadFinishCallVar;
Gk8Var iEmptyVar;
if(iLoadProgressCallVar.GetSize()==1 && iLoadProgressCallVar[0].IfInt()) SetEvent(sg_iLoadProgressEvent,iLoadProgressCallVar[0],iEmptyVar);
if(iLoadFinishCallVar.GetSize()==1 && iLoadFinishCallVar[0].IfInt()) SetEvent(sg_iLoadFinishEvent,iLoadFinishCallVar[0],iEmptyVar);
}
//[开始HTTP下载文件]
GK8_VOID Gk8HttpLoader::HttpLoaderFile()
{
//正在下载中
if(m_bLoading) return;
if(s_pLoaderCache->GetBufNum()<1)
{
//[通知脚本下载完成]
static Gk8Var sl_iParamVar;
sl_iParamVar.RemoveAll();
if(m_iLoadFinishCallVar.GetSize()==1)
{
RunEventWithArgs(sg_iLoadFinishEvent,sl_iParamVar);
}else
{
if(!m_iLoadFinishCallVar[0].IfPtr()||!IfObjPtr(m_iLoadFinishCallVar[0].GetPtr(),m_iLoadFinishCallVar[0].GetPtrId())) return;
Gk8Obj* pBindObj=m_iLoadFinishCallVar[0].GetSafePtr();
pBindObj->OnCall(m_iLoadFinishCallVar[1],sl_iParamVar);
}
m_nLoadFileCount=0;
m_nSumFileCount=0;
m_nLoadFileSize=0;
m_nSumFileSize=0;
return;
}
LPLOADERDATA lpLoaderData=s_pLoaderCache->GetBufEntryAt(0);
if(false==InitLoaderThreadSemphore()) return;
if(!lpLoaderData) return;
m_bLoading=true;
pthread_mutex_lock(&sg_RequestLoaderMutex);
sg_iRequestLoaderQueue.AddItem(lpLoaderData);
pthread_mutex_unlock(&sg_RequestLoaderMutex);
sem_post(sg_pLoaderSem);
}
//[处理进度条]
GK8_VOID Gk8HttpLoader::OnProgress(GK8_LPVOID lpLoaderData,GK8_CDOUBLE dlTotal,GK8_CDOUBLE dlNow)
{
LPLOADERDATA pLoaderData=(LPLOADERDATA)lpLoaderData;
static Gk8Var sl_iParamVar;
sl_iParamVar.RemoveAll();
sl_iParamVar<<m_nLoadShowCase<<pLoaderData->pFileName<<m_nLoadFileCount<<m_nSumFileCount
<<(m_nLoadFileSize+(GK8_INT)dlNow)<<m_nSumFileSize<<(GK8_INT)dlTotal<<(GK8_INT)dlNow;
if(m_iLoadProgressCallVar.GetSize()==1)
{
RunEventWithArgs(sg_iLoadProgressEvent,sl_iParamVar);
}else
{
if(!m_iLoadProgressCallVar[0].IfPtr()||!IfObjPtr(m_iLoadProgressCallVar[0].GetPtr(),m_iLoadProgressCallVar[0].GetPtrId())) return;
Gk8Obj* pBindObj=m_iLoadProgressCallVar[0].GetSafePtr();
pBindObj->OnCall(m_iLoadProgressCallVar[1],sl_iParamVar);
}
}
//[显示进度条]
GK8_VOID Gk8HttpLoader::ShowLoadBar(GK8_LPVOID lpLoaderData)
{
LPLOADERDATA pLoaderData=(LPLOADERDATA)lpLoaderData;
GK8_DOUBLE dlTotal=pLoaderData->nFileSize;
GK8_DOUBLE dlNow=pLoaderData->nFileSize;
OnProgress(lpLoaderData,dlTotal,dlNow);
}
//[获取HTTP下载器单例]
Gk8HttpLoader* Gk8HttpLoader::GetInstance()
{
if(sg_pHttpLoader==NULL) sg_pHttpLoader=new Gk8HttpLoader();
return sg_pHttpLoader;
}
GK8_VOID Gk8HttpLoader::Destroy()
{
if(s_pLoaderCache!=NULL)
{
delete s_pLoaderCache;
s_pLoaderCache=NULL;
}
}
//[HTTP下载器响应帧控制]
GK8_VOID Gk8HttpLoader::HttpLoaderTick()
{
if(sg_pLoaderSem==NULL) return;
LPLOADERDATA pLoaderData=NULL;
pthread_mutex_lock(&sg_ResponseLoaderMutex);
if(sg_iResponseLoaderQueue.GetSize()>0)
{
pLoaderData=sg_iResponseLoaderQueue.GetItemAt(0);
sg_iResponseLoaderQueue.RemoveItemAt(0);
}
pthread_mutex_unlock(&sg_ResponseLoaderMutex);
if(!pLoaderData) return;
//[把服务器数据派遣到脚本中]
Gk8Str iResponstStr;
if(!pLoaderData->bSucceed)
{
_GK8ERR<<"Gk8HttpLoader Response Failed Error Is "<<pLoaderData->sErrorBuffer<<" nResponseCode:"<<pLoaderData->nResponseCode<<CR;
iResponstStr="UnKown Error";
}else
{
//保存文件
Gk8File iFile;
if(iFile.Open(pLoaderData->pLocalFile,Gk8File::modeWriteAbs))
{
iFile.Write(pLoaderData->pFileBufData->GetBuf(),pLoaderData->pFileBufData->GetStreamSize());
iFile.Close();
}
}
m_nLoadFileCount++;
ShowLoadBar(pLoaderData);
m_nLoadFileSize+=pLoaderData->nFileSize;
//移除已下载数据:继续下载
s_pLoaderCache->RemoveEntryWithNameId(pLoaderData->nNameId);
m_bLoading=false;
HttpLoaderFile();
}