文章目录
  1. 1. 网络传输架构
  2. 2. CURL
  3. 3. 简单传输
  4. 4. 非阻塞传输

网络传输架构

在游戏中选择网络传输方式时需要慎重。通常,有两种可能的方式供我们选择,具体如下所示:

  1. 直接使用socket传输。通信的两端都使用一个特定的端口传输数据,传输面向的是字节流或数据包,需要处理的细节比较多,包括建立与关闭连接、设计与实现网络协议、维护传输通道的稳定性、监控数据传输速率等。因此,很多情况下,我们需要在socket传输之上再根据游戏需求包装一层操作协议,以降低使用复杂度
  2. 使用HTTP传输。直接用数据包的形式将数据提交到服务端的特定URL中,服务器同样用数据包的形式返回响应数据,其中大量的底层细节隐藏在了HTTP中。HTTP作为非对等、主从式的传输,需要建立对应的服务器,幸运的是,Web大潮催生了一批稳定、成熟的HTTP服务器框架,让我们可以方便地建立一个HTTP服务器。

游戏中涉及网络部分的传输一般采用中心服务器的架构,服务器以HTTP服务形式建立API服务,各移动终端向中心服务器请求所需的API获得服务。

CURL

CURL是Cocos2d-x推荐使用的网络传输库,随引擎代码一起分发了CURL的一份CPP实现。它是免费开源的,而且支持FTP、HTTP、LDAP等多种传输方式,同时横跨了Windows、UNIX、Linux平台,可以在各种主流的移动设备上良好工作。

它存在两套核心的接口,分别对应两种不同的使用方式,具体如下所示:

  1. 单线程传输的阻塞方式:每次处理一个传输请求,会一直阻塞当前线程直到传输完成。
  2. 非阻塞方式:允许同时提交一批传输请求,CURL会开启后台线程处理这些请求,传输结果的返回也是异步的。

简单传输

简单传输就是阻塞的单线程传输,使用方式相对简单,其接口也都是前缀为curleasy的形式,涉及4个常用API,如下所示:

CURL *curl_easy_init(void); //初始化一个传输
CURLcode curl_easy_setopt(CURL *curl, CURLoption option, ...); //设置传输参数
CURLcode curl_easy_perform(CURL *curl); //执行当前传输
void curl_easy_cleanup(CURL *curl); //清理

其中curl_easy_setopt函数的使用形式和一般的函数不太一样的是,没有对每一个参数配备独立的设置函数,而是通过定义不同的枚举常量传递参数设置,这和OpenGL中的glEnable系列函数非常类似,极大地简化了API接口。

我们沿用键值对的形式传输数据,这样的好处依然是简单灵活,只需通过POST形式向服务器提交数据即可。因此,根据实际使用,我们将对其作适当的二次封装,相关代码如下:

class NetworkAdaptor{
    string m_sBaseUrl;
public:
    NetworkAdaptor(const string& baseUrl);
    NetworkAdaptor(const char* baseUrl);
    bool sendValueForKey(const char* key, const char* _value, string& writeBackBuff);
    bool sendValuesForKey(const map<string, string>& values, string& writeBackBuff);
};

其中核心部分的sendValuesForKey函数向一个预设的URL传输一个字典中的所有键值对。只传输一对键值对的sendValueForKey可以在此基础上实现。sendValuesForKey函数的实现代码如下所示:

bool NetworkAdaptor::sendValuesForKey(const map<string,string>& values,string& writeBackBuff)
{
    CURL *curl = curl_easy_init();

    string sendout;
    translate(values, sendout);
    curl_easy_setopt(curl, CURLOPT_URL, m_sBaseUrl.c_str());
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, sendout.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeBackBuff);

    int res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);

    if (res == 0) {
         CCLOG("get data from server : %s", writeBackBuff.c_str());
        return true; 
    }
    else {
        CCLOG("curl post error!");
        return false;
    }
}

在这个函数中,我们用curl_easy_setopt设置了CURL的几个关键参数:设置发送POST请求、设置上传的数据、设置回调函数,以及设置缓冲对象。最后便是执行和清理了。

translate函数负责将传入的字典值按照POST的参数传递标准序列化为字符串,其实现代码如下:

void translate(const map<string, string>& values, string& sendoutMsg)
{
    sendoutMsg = "";
    for(map<string, string>::const_iterator it = values.begin();
        it != values.end(); ++it) {
        sendoutMsg += (it->first + " = " + it->second);
    }
}

而回调写函数则是根据CURL的回调标准编写,负责将data指向的nmemb个数据(每个数据的大小为size字节)写入writeData缓冲区内,并返回读取的总字节数。这里我们直接将传回的数据连接到一个缓冲字符串之后。而在实际开发中,这也是一个非常适合用于解码的地方,我们可以将传回的数据直接反序列化为需要操作的数据对象,相关代码如下:

size_t writer(char* data, size_t size, size_t nmemb, string* writerData)
{
    LOG_FUNCTION_LIFE;
    if(writerData == NULL)
        return 0;

    writerData->append(data, size * nmemb);
    return size * nmemb;
}

非阻塞传输

前面看到的整个网络传输过程是阻塞串行执行的,尽管设置了回调函数,但也只是为了应对间断到达的数据流,代码之间实际上不存在乱序执行的可能。在传输量稍大的情况下,例如初始化一些场景实时请求资源时,或是对游戏进行大规模升级时,阻塞主线程会导致画面停滞,这在实际开发中是绝对不允许的。另外,网络传输速度毕竟是有限的。即使网络繁忙时,系统内的大部分CPU和内存资源也都是空闲的。因此,我们需要引入非阻塞的网络传输。

CURL是支持非阻塞传输的,而且还允许并行地进行多个网络请求,其接口主要是以curl_multi为前缀的系列的函数:

CURLM *curl_multi_init(void); //初始化
CURLMcode curl_multi_add_handle(CURLM *multi_handle,
CURL *curl_handle); //添加一个传输请求
CURLMcode curl_multi_perform(CURLM *multi_handle, int *running_handles); //执行传输
CURLMcode curl_multi_cleanup(CURLM *multi_handle); //清理

typedef map<string, string> StringMap;
class AsynchronousNetworkAdaptor
{
protected:
    struct RequestInfo
    {
        RequestInfo(const StringMap& _v, const string& _u, string& _b): values(_v), url(_u), buffer(_b) { }

        StringMap values;
        string url;
        string& buffer;
};
    vector<RequestInfo> requests;

public:

    void sendValueForKeyToURL(const char* key, const char* _value,
    const string& url, string& writeBackBuff);
    void sendValuesForKeyToURL(const StringMap& values,
    const string& url, string& writeBackBuff);
    void flushSendRequest();

    CC_SYNTHESIZE_READONLY(int, m_iUnfinishedRequest, UnfinishedRequest);
};

每个请求到达后,我们仅仅将其缓冲到一个数组中:

void AsynchronousNetworkAdaptor::sendValuesForKeyToURL(const StringMap& values, const string& url, string&writeBackBuff){
    RequestInfo info(values, url, writeBackBuff);
    requests.push_back(info);
}

随后,在flushSendRequest函数内一次性地将所有的请求发出,相关代码如下:

void AsynchronousNetworkAdaptor::flushSendRequest()
{
    CURLM* backUrl = curl_multi_init();

    for(int i = 0; i < requests.size(); i++) {
        CURL *curl = curl_easy_init();

        curl_easy_setopt(curl, CURLOPT_URL, requests[i].url);
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
        curl_easy_setopt(curl, CURLOPT_POST,1);

        string sendout;
        translate(requests[i].values, sendout);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS,sendout.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &requests[i].buffer);

        curl_multi_add_handle(backUrl, curl);
        curl_easy_cleanup(curl);
    }
    curl_multi_perform(backUrl, &m_iUnfinishedRequest);
}
文章目录
  1. 1. 网络传输架构
  2. 2. CURL
  3. 3. 简单传输
  4. 4. 非阻塞传输