Coco2dx 使用sqlite數據庫存儲以及加密數據總結

我還記得今年年初的項目中,我們對數據的讀取都是從execl導出爲json文件,通過解析json文件信息,來獲取遊戲的配置信息。這樣做簡單但是當我們插入或者是刪除其中的某個數據項的時候,修改起來比較的麻煩而且容易出現錯誤,而且不容易查錯。而後在網上看着別人分享使用sqlite數據庫的方案,就果斷去嘗試。

對於sqlite數據庫本身我就不多說了,這裏主要是總結在遊戲中使用sqlite數據步奏和方法。首先需要下載sqlite數據的界面可視化工具Navicat Premium,有了可視化工具,我們新建表和其中的表項就變得十分的容易和簡單了。使用的界面和mysql數據可視化界面基本上差不多。更多情況下我們都需要對數據庫進行加密,可以參考http://www.cocoachina.com/bbs/read.php?tid=199953

首先是打開數據庫的操作:

void DataCtrl::openDB()
{
    if(FileOperation::isFileExistInWRDir("data/test.sqlite") == false) //驗證遊戲的可讀寫區域是否存在該數據庫
    {
        FileOperation::copyData("test.sqlite","data"); //從本地拷貝到可讀寫區域
    }

    sqlite3 * db;
    std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
    
    if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK) //open database success
    {
        cocos2d::log("open database success!");
    }
    else
    {
        cocos2d::log("open database failed!");
    }
}
我們本地新建一個test.sqlite放在Resources的data目錄下,遊戲首次運行時,數據庫是不在遊戲的可讀取區域的,我們需要從本地拷貝進去。


    bool FileOperation::isFileExistInWRDir(const char* fileName)
    {
        if( !fileName ) return false;
        std::string filePath = FileUtils::getInstance()->getWritablePath();
        filePath += fileName;
        
        FILE *fp = fopen(filePath.c_str(),"r");
        if(fp)
        {
            fclose(fp);
            return true;
        }
        return false;
    }
    
    void FileOperation::copyData(const char* fileName, const char* dirName)
    {
        std::string symbol = "/";
        
        std::string strPath = FileUtils::getInstance()->fullPathForFilename(dirName + symbol + fileName);
        ssize_t len = 0;
        unsigned char *data = NULL;
        
        data = FileUtils::getInstance()->getFileData(strPath.c_str(),"r",&len);
        std::string destPath = FileUtils::getInstance()->getWritablePath();
        
        
        //目錄不存在,則創建
        if(FileUtils::getInstance()->isDirectoryExist(destPath + dirName) == false)
        {
            FileUtils::getInstance()->createDirectory(destPath + dirName);
        }
        
        destPath += dirName + symbol + fileName;
        FILE *fp = fopen(destPath.c_str(),"w+");
        if(fp != nullptr)
        {
            fwrite(data,sizeof(char),len,fp);
            fclose(fp);
            delete []data;
            data = NULL;
            
            CCLOG("Copy %s to WRDir Compeleted!", fileName);
        }
        else
        {
            if(data)
                delete []data;
            
            CCLOG("Copy %s to WRDir Failed!", fileName);
        }
    }

這是我們自己封裝的文件操作類的兩個方法。


好了數據庫到是打開了,我們現在來驗證數據的加密與解密。首先是加密

void HelloWorld::encryptDB()
{
    std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
    sqlite3 *db;
    char pwd[100] = dbPassWord;//密碼的宏定義
    
    if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK)
    {
        printf("1.打開未加密數據庫成功,正在加密……\n");
        sqlite3_rekey(db,pwd,static_cast<int>(strlen(pwd)));
        sqlite3_close(db);
    }
    else
    {
        printf("打開未加密數據庫失敗\n");
    }
    
    if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK)
    {
        if(sqlite3_key(db,pwd,static_cast<int>(strlen(pwd))) == SQLITE_OK)
        {
            printf("2.驗證加密,加密成功\n");
        }
        else
        {
            printf("加密失敗\n");
        }
    }
    sqlite3_close(db);

}

在執行以上代碼後,我們會發現test.sqlite已經不能打開了,說明加密成功了!對於加密的具體過程,可以去追究sqlite的源碼(注意此時我們都是對可讀寫區域的數據庫進行的操作)


然後進行數據庫的解密測試:


void HelloWorld::decodingDB()
{
    std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
    cocos2d::log("%s",FileUtils::getInstance()->getWritablePath().c_str());
    sqlite3 *db;
    char pwd[100] = dbPassWord;
    if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK) //首先用密碼打開數據庫之後,然後再去除密碼
    {
        if(sqlite3_key(db,pwd,static_cast<int>(strlen(pwd))) == SQLITE_OK)
        {
            printf("2.驗證加密,加密成功\n");
            if(sqlite3_rekey(db,NULL,0) == SQLITE_OK)
            {
                printf("3.解密數據庫成功……\n");
                sqlite3_close(db);
            }
        }
        else
        {
            printf("加密失敗\n");
        }
    }

}

剛開始使用出現錯誤,原來是加密之後的數據庫必須使用密鑰才能打開,這樣就導致在代碼中能看到我們的密碼。如果要隱藏密碼可以採取線上獲取,或者是本地數據與線上的數據庫來進行比對,如果出現衝突,以線上數據庫爲準。


數據的加密肯定是在之前就做好處理的,所以我們還得將以上代碼增加封裝到工具當中去,在本地進行數據的加密和解密。因爲當我們編輯數據的時候,數據庫就不能加密,而遊戲中使用的數據庫需要加密。


當然此時我們把數據庫操作類進行單獨的封裝

#include "SQLiteManage.hpp"


SQLiteManage * SQLiteManage::m_Instance = nullptr;

sqlite3 * SQLiteManage::m_SqliteDB = nullptr;

SQLiteManage::SQLiteManage()
{
    
}

SQLiteManage::~SQLiteManage()
{
    if(m_SqliteDB)
    {
        sqlite3_close(m_SqliteDB);
        m_SqliteDB = nullptr;
    }
}

SQLiteManage * SQLiteManage::getInstance()
{
    if(m_Instance == nullptr)
    {
        m_Instance =  new SQLiteManage();
    }
    return m_Instance;
}

bool SQLiteManage::openDB(std::string dbName, std::string password)
{
    if(m_SqliteDB != nullptr)
        closeDB();
    std::string dbPath = cocos2d::FileUtils::getInstance()->getWritablePath() + dbName;
    
    int result = -1;
    
    result = sqlite3_open(dbPath.c_str(), &m_SqliteDB);
    
    if(result == SQLITE_OK)
    {
        if(password.length() != 0)
        {
            if(sqlite3_key(m_SqliteDB, password.c_str(), (int)password.length())!= SQLITE_OK)
            {
                CCLOG("open database failed! password error!");
                return false;
            }
        }
        return true;
    }
    else
    {
        CCLOG("open databse failed! error code = %d",result);
        return false;
    }
}

void SQLiteManage::closeDB()
{
    if(m_SqliteDB != nullptr)
        sqlite3_close(m_SqliteDB);
    m_SqliteDB = nullptr;
}

bool SQLiteManage::execSQL(std::string strSql, int(*callback)(void *,int,char **,char**))
{
    if(m_SqliteDB == nullptr)
    {
        CCLOG("please open the database at first!");
        return false;
    }
    char *errorMsg;
    
    int result = sqlite3_exec(m_SqliteDB, strSql.c_str(), callback, NULL, &errorMsg);
    if(result != SQLITE_OK)
    {
        CCLOG( "exec a sql error, error code:%d ,error msg:%s", result, errorMsg);
    }
    return true;
}

其中callback回調函數必須輸靜態的函數,而且數據讀取時一行則調用一次回調函數,所以我們需要在回調中把每行的數據放在早已定義好的數據結構當中去!而這個過程我們還可以進行優化,而且還需要增加一個數據管理類,對於遊戲配置數據,不應該提供寫的接口,玩家數據需要可讀取的接口。當然了,這個類只給出了基本的數據庫操作,可以根據實際情況增加我們需要的操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章