mangos分析

来源:互联网 发布:淘宝开个店铺多少钱 编辑:程序博客网 时间:2024/05/21 07:55
MaNGOS之验证Realm登录服务器(2011-03-12 21:26:30)
转载
标签:

mangos

srp6

sha

分类: Mangos代码阅读

用的是srp6算法加密的

 

1.客户端先发送AUTH_LOGON_CHALLENGE消息,其中主要含有用户名,客户端版本号

 

2. 服务端接受到消息后,首先进行以下check

1)该ip是否被封,若封发相应错误

2)查看是否有该账户,若无发相应错误

3)查看最后一次登陆ip与账户是否绑定

若绑定 1>当前ip与last ip相同则ok 2>不同则发相应错误

若不绑定也ok

4)查看帐号是否被封,若被封发相应错误

5)获取用户名,开始SRP6计算

 

// multiply with 2, bytes are stored as hexstring
if(databaseV.size() != s_BYTE_SIZE*2 || databaseS.size() != s_BYTE_SIZE*2)
   _SetVSFields(rI); //若未在database中设s,v,则调用该方法
else
{
    s.SetHexStr(databaseS.c_str()); //直接去database中的s,v
    v.SetHexStr(databaseV.c_str());
}

/// Make the SRP6 calculation from hash in dB
void AuthSocket::_SetVSFields(const std::string& rI)
{
    s.SetRand(s_BYTE_SIZE * 8); // s为32可字节的随机数

    BigNumber I;
    I.SetHexStr(rI.c_str()); //rI为 username:password的sha摘要

    // In case of leading zeros in the rI hash, restore them
    uint8 mDigest[SHA_DIGEST_LENGTH];
    memset(mDigest, 0, SHA_DIGEST_LENGTH);
    if (I.GetNumBytes() <= SHA_DIGEST_LENGTH)
        memcpy(mDigest, I.AsByteArray(), I.GetNumBytes());

    std::reverse(mDigest, mDigest + SHA_DIGEST_LENGTH); //将username:password的sha摘要倒序

    Sha1Hash sha;
    sha.UpdateData(s.AsByteArray(), s.GetNumBytes());
    sha.UpdateData(mDigest, SHA_DIGEST_LENGTH);
    sha.Finalize();// 计算x = sha(s,username:password的sha摘要倒序)

    BigNumber x;
    x.SetBinary(sha.GetDigest(), sha.GetLength());
    v = g.ModExp(x, N); // v= g^x mod N  (g = 7, N 是个很长的自然数)
    // No SQL injection (username escaped)
    const char *v_hex, *s_hex;
    v_hex = v.AsHexStr();
    s_hex = s.AsHexStr();
    loginDatabase.PExecute("UPDATE account SET v = '%s', s = '%s' WHERE username = '%s'", v_hex, s_hex, _safelogin.c_str() );
    OPENSSL_free((void*)v_hex);
    OPENSSL_free((void*)s_hex);
}

小结一下

在服务端计算s,v

s为32个字节的随机数

v= g^x mod N  (g = 7, N 是个很长的自然数) x是s,用户名的字节序列倒置的sha哈希值

s,v存到数据库

 

接着计算B (服务端公钥)

B = ((v*3) + gmod) % N;

gmod = g^b mode N

b 为19个字节的随机数

 

向客户端发送B,g,N,s

 

 

在客户端收到B,g,N,s后

计算A 客户端公钥

A = g^a mode N

a为19为随机数


计算x

x = sha(s,I)

I = sha("username:password")

 

计算u

u=sha(A,B)// 公钥 (服务公钥,客户公钥)

 

计算S

S = (B - g^x*3)^(a+u*x)

计算K

S为32位,K为40位

是 sha(s奇部分)20位, sha(s偶部分)20位的奇偶交错组合

 

计算M,服务端也将有一套算法试图计算这个值,若于之相同则通过验证

t3 = sha(N)[i] sha(g)[i]

t4 = sha(username)

M = sha(t3,t4,s,A,B,K)

 

向服务端发送A,M


服务端接受到消息后

1)检查客户端版本,不支持的版本则报错

2) SRP6验证

 

计算S = (A * (v^u mode N ))^b mode N  

u = sha(A,B)

v= g^x mod N

x = sha(s,db中存的sha(username:password)倒序)

//对应客户端S = (B - g^x*3)^(a + u*x)

同样计算K = Interleave(S), 计算t3,t4

M = sha(t3,t4,s,A,B,K)

 

与客户端传来的M比较,相同则验证成功

 

总结

关键在于服务端,客户端各自计算S的公式个不同

虽然公钥部分服务端用A,b, 客户端用B,a 但其计算结果是相同的

 

x这个私钥很好地得到了隐藏

服务端传B,g,N,s

客户端传A,M

要从M获取私钥x难比登天,所以即使拦截了所有客户端服务端的会话也无法破解密码

 

Mangos之SMSG_COMPRESSED_UPDATE_OBJECT消息

(2011-03-09 14:50:50)
转载
标签:

压缩算法

登陆游戏

封装

数据量

加载

it

分类: Mangos代码阅读


当玩家登陆游戏服务器后执行以下操作(由CharacterHandler类负责)

加载玩家信息 

Player *pCurrChar = new Player(this);

pCurrChar->LoadFromDB(GUID_LOPART(playerGuid), holder)

 

这两行将加载所有玩家信息,包括地图块

 

然后将向地图块中添加玩家

   if (!pCurrChar->GetMap()->Add(pCurrChar))

在该行执行过程中会向客户端发送玩家初始化数据

    SendInitSelf(player); //Map类的方法

初始化数据量比较大,包括玩家属性,位置,物品,装备等等。

所以需要压缩

 

    UpdateData data;

    // build data for self presence in world at own client (one time for map)
    player->BuildCreateUpdateBlockForPlayer(&data, player); //封装各种玩家相关对象数据

    WorldPacket packet;
    data.BuildPacket(&packet);
    player->GetSession()->SendPacket(&packet);

 

UpdateData类用于封装这样的大数据包,其中包含压缩算法(调用了zlib)

 

bool UpdateData::BuildPacket(WorldPacket *packet)
{
    ASSERT(packet->empty());                                // shouldn't happen

    ByteBuffer buf(4 + (m_outOfRangeGUIDs.empty() ? 0 : 1 + 4 + 9 * m_outOfRangeGUIDs.size()) + m_data.wpos());

    buf << (uint32) (!m_outOfRangeGUIDs.empty() ? m_blockCount + 1 : m_blockCount);

    if(!m_outOfRangeGUIDs.empty())
    {
        buf << (uint8) UPDATETYPE_OUT_OF_RANGE_OBJECTS;
        buf << (uint32) m_outOfRangeGUIDs.size();

        for(std::set<uint64>::const_iterator i = m_outOfRangeGUIDs.begin(); i != m_outOfRangeGUIDs.end(); ++i)
        {
            buf.appendPackGUID(*i);
        }
    }

    buf.append(m_data);

    size_t pSize = buf.wpos();                              // use real used data size

    if (pSize > 100 )                                       // compress large packets
    {
        uint32 destsize = compressBound(pSize);
        packet->resize( destsize + sizeof(uint32) );

        packet->put<uint32>(0, pSize);
        Compress(const_cast<uint8*>(packet->contents()) + sizeof(uint32), &destsize, (void*)buf.contents(), pSize);
        if (destsize == 0)
            return false;

        packet->resize( destsize + sizeof(uint32) );
        packet->SetOpcode( SMSG_COMPRESSED_UPDATE_OBJECT );
    }
    else                                                    // send small packets without compression
    {
        packet->append( buf );
        packet->SetOpcode( SMSG_UPDATE_OBJECT );
    }

    return true;
}

 

 

Mangos之异步数据库查询

(2011-01-28 19:55:42)
转载
标签:

mangos

异步sql查询

callback框架

分类: Mangos代码阅读

为什么需要异步数据库查询?

来看一下如果两个执行顺序:

顺序1:

执行sql语句1;

对应sql语句1结果执行的动作;

执行sql语句2;

对应sql语句2结果执行的动作;

。。。。。。。。。。。

 

顺序2:

在线程1中

执行sql语句1;

执行sql语句2;

.........

 

在线程2中

添加sql语句1到线程1;

添加sql语句2到线程1;

.......... // 线程循环

对应sql语句2结果执行的动作; //次序可以是随机的,只要sql语句结果返回就对其做相应的动作

对应sql语句1结果执行的动作;

 

可见,顺序2有一下优点

性能更

高类似的操作集中执行

响应更快

顺序1中执行一次sql语句后紧跟着相应的处理动作,如果当前sql语句耗时很长,下一个sql语句耗时又很短,那么下一个耗时不长的sql语句的相应方法就必须等待当前sql语句及相应的处理动作执行完后才能执行.

 

在Mangos中,对数据库characters的操作就使用了异步sql,大概是因为mangos对于该数据库的操作比较平凡且响应速度要求比较高,设想上千个玩家登陆服务器,每时每刻的变化都要保存到characters数据库中,并且其他玩家变化及时地反映到游戏世界中。

 

先来看一下mangos中异步sql的使用方法:

void WorldSession::HandleCharEnumOpcode( WorldPacket & )
{
//get all the data necessary for loading all characters (along with their pets) on the account
//第一个参数&chrHanler是包含一组回调方法的类对象

//第二个参数则是该类中某个回调方法,这个回调方法以sql语句执行的结果QueryResult作为参数

//第三个参数用作回调方法的第二个参数

//第四个参数是sql语句的format形式

//下面的多个参数是sql语句format中用到的变量

CharacterDatabase.AsyncPQuery(&chrHandler, &CharacterHandler::HandleCharEnumCallback, GetAccountId(),"sql format", PET_SAVE_AS_CURRENT, GetAccountId());
}

 

来看一下这个专门用来处理角色信息的handler类:

class CharacterHandler
{
    public:

        //传入AsyncPQuery回调方法的参数有两个: sql的执行结果,和用于定位会话的accountId;

        //当sql语句执行完后返回结果准备好后,该方法在某时刻将得到调用
        void HandleCharEnumCallback(QueryResult * result, uint32 account)
        {
            WorldSession * session = sWorld.FindSession(account);
            if(!session)
            {
                delete result;
                return;
            }

            //由于这类操作都是会话相关的,实际方法放在Session中更为简便
            session->HandleCharEnum(result);
        }
        void HandlePlayerLoginCallback(QueryResult * , SqlQueryHolder * holder)
        {
            if (!holder) return;
            WorldSession *session = sWorld.FindSession(((LoginQueryHolder*)holder)->GetAccountId());
            if(!session)
            {
                delete holder;
                return;
            }
            session->HandlePlayerLogin((LoginQueryHolder*)holder);
        }
} chrHandler;

具体回调运行过程在WorldSession中:

void WorldSession::HandleCharEnum(QueryResult * result)
{
    WorldPacket data(SMSG_CHAR_ENUM, 100);                  // we guess size

    uint8 num = 0;

    data << num;

    if( result )
    {
        do
        {
            uint32 guidlow = (*result)[0].GetUInt32();
            sLog.outDetail("Loading char guid %u from account %u.", guidlow, GetAccountId());

            //构建角色信息
            if(Player::BuildEnumData(result, &data))
                ++num;
        }
        while( result->NextRow() );

        delete result;
    }

    data.put<uint8>(0, num);

    SendPacket( &data ); //以该会话发送数据
}

 

总体上的调用流程是上这样的:

在游戏主线程的循环体中会调用世界对象sWorld的update方法,其中做了下面这些事

    /// <li> Handle session updates when the timer has passed
    if (m_timers[WUPDATE_SESSIONS].Passed())
    {
        m_timers[WUPDATE_SESSIONS].Reset();

        UpdateSessions(diff);  //循环每个会话,调用session的update方法,若有数据包,会解密数据包头,拿到opCode,找到并掉用相应的处理方法,比如HandleCharEnumOpcode(WorldPacket& ),它运行了AsyncPQuery将sql语句放入执行队列,CharacterDatabase中的异步sql执行线程会执行该sql,并把执行结果法如结果队列中。
    }

    。。。。。。。。。。。。。。。。。。。

    // execute callbacks from sql queries that were queued recently

    //在主线程中此处完成一系列的sql语句执行后的反馈方法
    UpdateResultQueue();//这个方法很简单,就执行了m_resultQueue->Update();

 

来看一下结果队列的update方法

void SqlResultQueue::Update()
{
    /// execute the callbacks waiting in the synchronization queue
    MaNGOS::IQueryCallback* callback;
    while (next(callback)) //循环每个结果队列,拿出对应的反馈方法
    {
        callback->Execute(); //执行反馈方法,这里很神奇吧,是怎么做到的呢?稍后会讨论
        delete callback;
    }
}

 

CharacterDatabase中的异步sql实现:

上面大致介绍了异步sql的执行流程,现在看一下它是如何实现的

1.异步sql执行线程

CharacterDatabase在初始化时会开启一个专门用于执行异步sql的线程

 

void DatabaseMysql::InitDelayThread()
{
    assert(!m_delayThread);

    //New delay thread for delay execute
    m_threadBody = new

(this); //异步sql执行线程的方法体
    m_delayThread = new ACE_Based::Thread(m_threadBody); //这个构造函数将参数赋给Thread的m_task,并启动线程
}

 

下面是这个线程的类图设计:

Mangos之异步数据库查询

 

类Thread是对ACE线程操作的wrapper,它包含了启动,关闭线程等功能,其构造函数为要开启的Thread赋予线程执行体并开启线程,具体实现这里就不讨论了,都是ACE Developemnt Guide上找的到的。

 

关键是SqlDelayThread,线程的方法体。

成员m_dbEngine是DatabaseMysql的实例,DatabaseMysql对mysqlapi做了封装,其中除提供基本sql操作功能外,还包含了像AsyncQuery,transaction相关的方法,这里需要这个引用是为了能执行sql基本操作。

 

成员m_sqlQueue是个线程安全的sql操作队列,sql语句被封装成SqlOperation之后放入该队列。

 

方法 Delay(SqlOperation* sql)将sql操作置入操作队列中

 

线程方法体定义:

void SqlDelayThread::run()
{
    #ifndef DO_POSTGRESQL
    mysql_thread_init();
    #endif

    while (m_running)
    {
        // if the running state gets turned off while sleeping
        // empty the queue before exiting
        ACE_Based::Thread::Sleep(10);
        SqlOperation* s;

        //从队列中获取下一个sqlOperation并执行
        while (m_sqlQueue.next(s))
        {
            s->Execute(m_dbEngine);
            delete s;
        }
    }

    #ifndef DO_POSTGRESQL
    mysql_thread_end();
    #endif
}

SqlOperation的类图设计

Mangos之异步数据库查询
SqlOperation是抽象类,具体操作如查询操作SqlQuery实现了它。

成员m_callback是回调函数接口

m_queue则是sql语句执行完毕后生成的结果队列,同样是线程安全的

 

前面已经提到过,显而易见,sql执行线程中主要用到SqlOperation的Execute方法。

void SqlQuery::Execute(Database *db)
{
    if(!m_callback || !m_queue)
        return;
    /// execute the query and store the result in the callback
    m_callback->SetResult(db->Query(m_sql)); //调用sql基本操作,并将结果放入回调对象中
    /// add the callback to the sql result queue of the thread it originated from
    m_queue->add(m_callback);//将回调对象放入结果队列中。
}

 

前文不知不觉中多次用了两个概念: 回调对象,结果队列

其实回调对象是真正的精华所在,而结果队列也只是建立在回调对象之上的东东,其设置和使用也值得一提

 

看一下SqlQuery的构造函数
 SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue)

后面二个对象分别就是回调对象,结果队列。

 

回调对象内容比较多,先来看看结果队列:

Mangos之异步数据库查询

LockedQueue是个模板类,用于多线程的queue,SqlResultQueue继承LockedQueue以IQueryCallback为容器对象,ACE_Thread_Mutex为锁类型的模板实例,它还包含一个Update方法。

 

void SqlResultQueue::Update()
{
    /// execute the callbacks waiting in the synchronization queue
    MaNGOS::IQueryCallback* callback;
    while (next(callback)) //从结果队列中获取每个callback对象并执行
    {
        callback->Execute();
        delete callback;
    }
}

 

之前提到过主线程中某处调用了UpdateResultQueue();

对就是在这里调用SqlResultQueue::Update方法

 

也许聪明的你会发现,主线程调用UpdateResultQueue,往ResultQueue放回调对象的是sqlDelayThread,

多个线程共用了ResultQueue,那么另一个线程(非主线程和sqlDelayThread)也许也有需要使用异步sql,而sqlDelayThread是否可以被共享呢?

 

有了这样的需求,DataBaseMysql有如下的设计


Mangos之异步数据库查询

Database为抽象类,DatabaseMysql用mysql api实现了Database

注意成员m_queryQueues是个线程指针和结果队列SqlResultQueue,也即是说DatabaseMysql保存来自各个线程的结果队列,而每个sqlOperation也知道自己所属的结果队列,如

SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue) //第三个参数

所以SqlOperation执行结果会被放入它所属的结果队列中,而sqlDelayThread不区分结果队列地执行每个SqlOperation。

这样就构成了sqlDelayThread为多个线程所共享,每个线程负责创建自己的结果队列,在创建SqlOperation时指定自己的结果队列,并在自己线程的适当地方调用UpdateResultQueue。

 

看一下World世界对象对结果队列的操作。

void World::InitResultQueue()
{
    m_resultQueue = new SqlResultQueue; //创建输入自己线程的结果队列
    CharacterDatabase.SetResultQueue(m_resultQueue); //将结果队列设置到CharacterDatabase
}

void Database::SetResultQueue(SqlResultQueue * queue)
{
    m_queryQueues[ACE_Based::Thread::current()] = queue; //thread*,resultQueue的map

}

 

回过头来看一下AsyncQuery这个方法

template<class Class, typename ParamType1>
bool Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)

 

方法体中使用了预定义宏

#define ASYNC_QUERY_BODY(sql, queue_itr) \
    if (!sql) return false; \
    \
    QueryQueues::iterator queue_itr; \
    \
    { \
        ACE_Based::Thread * queryThread = ACE_Based::Thread::current(); \ //获取当前线程指针
        queue_itr = m_queryQueues.find(queryThread); \ //找到改线程指针的结果队列
        if (queue_itr == m_queryQueues.end()) return false; \
    }

 

回调对象是最为精髓的地方,因为它能将一个包含一些回调方法的类(它继承任何其他类)封装为回调接口MaNGOS::IQueryCallback * callback 的实例。

 

template<class Class, typename ParamType1>
bool
Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)
{
    ASYNC_QUERY_BODY(sql, itr)
    return m_threadBody->Delay(new SqlQuery(sql, new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1), itr->second));
}

AsyncQuery的重载方法是个template方法

第一个参数根据模板的不同而不同, 但使用是不用指定<class Class>,第一个参数直接放入某个类的执政,

c++会在编译时生成用这个类的执政作为第一个参数的方法代码。

第二个参数是相应第一个参数指定的类型域的某个方法,它以QueryResult作为第一个参数,后面可以是多个模板参数ParamType1,ParamType2,也可以没有模板参数

这里列出的只有ParamType1,这是由于本文开头例子用的是

                                                                                      //ParamType1

CharacterDatabase.AsyncPQuery(&chrHandler, &CharacterHandler::HandleCharEnumCallback, GetAccountId(),"sql format", PET_SAVE_AS_CURRENT, GetAccountId());

 

因为CharacterHandler::HandleCharEnumCallback需要ParamType1这个参数来找到属于自己的会话

 

void HandleCharEnumCallback(QueryResult * result, uint32 account)
        {
            WorldSession * session = sWorld.FindSession(account);

 

当有如上两条语句是,c++根据模板类自动生成相应的代码。

 

下面解读AsyncQuery中的这条语句:

m_threadBody->Delay(new SqlQuery(sql, new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1), itr->second));

 

关键是new SqlQuery的第二个参数

 

看一下其原型

 SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue)

第二个参数是IQueryCallback接口

 

AsyncQuery中为它赋的是,所以重点解释一下这条语句

new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1)

 

为方便,再看一下相应的AsyncQuery原型
template<class Class, typename ParamType1>

bool Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)

 

放入MaNGOS::QueryCallback的第一个参数object

类型为Class *object

值为&chrHandler //class CharacterHandler的对象

具体类型为CharacterHandler

 

放入MaNGOS::QueryCallback的第二个参数method

类型是void (Class::*method)(QueryResult*, ParamType1)

值为 &CharacterHandler::HandleCharEnumCallback

具体类型为HandleCharEnumCallback的原型:

 void HandleCharEnumCallback(QueryResult * result, uint32 account)

 

放入MaNGOS::QueryCallback的第三个参数NULL

它不是模板参数,只有具体类型为QueryResult,值为NULL

 

放入MaNGOS::QueryCallback的第四个参数param1

类型ParamType1 param1,值为GetAccountId()返回结果,具体类型是uint32

 

 

看一下QueryCallback 的原型:

template < class Class, typename ParamType1 >
class QueryCallback < Class, ParamType1 > :public _IQueryCallback< _Callback < Class, QueryResult*, ParamType1 > >{
    private:
      typedef _Callback < Class, QueryResult*, ParamType1 > QC1;
    public:
      QueryCallback(Class *object, typename QC1::Method method, QueryResult* result, ParamType1 param1): _IQueryCallback< QC1 >(QC1(object, method, result, param1)) {}
};

那上面的模板参数类型与原型参数类型比对

第一个参数类型:

Class *object                                    Class *object

第二个参数类型:

void (Class::*method)(QueryResult*, ParamType1)  _Callback < Class, QueryResult*, ParamType1 >::Method

第三个是具体参数类型:

QueryResult* result                              QueryResult* result

第四个参数类型:

ParamType1 param1                                ParamType1 param1

 

只有第二个参数类型需要研究一下, _Callback 的定义

template < class Class, typename ParamType1, typename ParamType2 >
class _Callback < Class, ParamType1, ParamType2 >
{
   protected:
     typedef void (Class::*Method)(ParamType1, ParamType2);
     Class *m_object;
     Method m_method;
     ParamType1 m_param1;
     ParamType2 m_param2;
     void _Execute() { (m_object->*m_method)(m_param1, m_param2); }
   public:
     _Callback(Class *object, Method method, ParamType1 param1, ParamType2 param2)
        : m_object(object), m_method(method), m_param1(param1), m_param2(param2) {}
     _Callback(_Callback < Class, ParamType1, ParamType2 > const& cb)
        : m_object(cb.m_object), m_method(cb.m_method), m_param1(cb.m_param1), m_param2(cb.m_param2) {}
};

 

_Callback < Class, QueryResult*, ParamType1 >::Method

以实例化模板中类型

void (Class::*Method)(ParamType1, ParamType2)

 

所以Method 就是 void (Class::*method)(QueryResult*, ParamType1) 这个类型。

 

这里复杂性在于它用一个模板参数实例化另一个模板

 

讲的有点乱,画个图说明一下:

Mangos之异步数据库查询

首先模板实例化的是QueryCallback
Class为CharacterHandler
ParamType1为uint32

 

QueryCallback中又模板实例化_Callback

这样CharacterHandler与_Callback进行了绑定,它是能真正执行代码的类

void _Execute(){(m_object->*m_method)(m_param1,m_param2);}//执行callback方法

 

QueryCallback继承_IqueryCallback
_IqueryCallback也需要模板实例化,用的模板参数是刚才实例化的callback QC1

_IqueryCallback继承 QC1 和 接口IQueryCallback
其中实现了IQueryCallback的方法

void Execute()(CB:_Execute();) //CB为模板参数,被赋值QC1
void SetResult(QueryResult* result){CB::m_param1 = result;}
QueryResult* GetResult(){return CB::m_param1;}

 

这样一来QueryCallback就等同于用CharacterHandler的方法实现了IQueryCallback

 

AsyncPQuery方法体中

new MaNGOS::QueryCallback<Class, ParamType1>(object, method)这个语句后,就等同于new了一个

封装了CharacterHandler的IQueryCallback接口的实例。

 

在调用时可利用多态的特性调用每个IQueryCallback所绑定的Handler方法了。

    MaNGOS::IQueryCallback* callback;
    while (next(callback)) 
    {
        callback->Execute(); // 怎样,现在来看不再神奇了吧。 若callback绑定的是CharacterHandler则会调用它的回调方法。

        delete callback;
    }

 

写这个真累,虽然知道是写给自己看的。。。。。。。。。。。

 

Mangos服务器会话WorldSession

(2011-01-24 08:57:32)
转载
标签:

worldsession

mangos

分类: Mangos代码阅读

所有的游戏中的操作如角色移动,释放技能等都是要通过在会话中消息传递来进行。

 

当客户端通过服务器验证后,便与之建立起会话。下面是套接字handler: WorldSocket中HandleAuthSession方法的部分代码:

int WorldSocket::HandleAuthSession (WorldPacket& recvPacket)
{

    。。。。。。。。。。。

    //以上是验证客户端身份,成功则作下面操作

    // NOTE ATM the socket is single-threaded, have this in mind ...
    ACE_NEW_RETURN (m_Session, WorldSession (id, this, AccountTypes(security), expansion, mutetime, locale), -1); //创建会话对象,参数分别为帐号id,WorldSocket对象,帐号安全级别,游戏扩展,禁言时间,区域设置

    m_Crypt.Init(&K);

    m_Session->LoadGlobalAccountData(); //各种帐号设置
    m_Session->LoadTutorialsData(); 
    m_Session->ReadAddonsInfo(recvPacket);

    // In case needed sometime the second arg is in microseconds 1 000 000 = 1 sec
    ACE_OS::sleep (ACE_Time_Value (0, 10000));

    sWorld.AddSession (m_Session); //添加会话到world对象

 

游戏主线程会不断地调用sWorld的UpdateSessions()

 

void World::UpdateSessions( uint32 diff )
{
    ///- Add new sessions
    WorldSession* sess; 
    while(addSessQueue.next(sess))
        AddSession_ (sess);  //若有新会话则加入

    ///- Then send an update signal to remaining ones
    for (SessionMap::iterator itr = m_sessions.begin(), next; itr != m_sessions.end(); itr = next)
    {
        next = itr;
        ++next;
        ///- and remove not active sessions from the list
        if(!itr->second->Update(diff)) //调用每个会话的update()方法
        {
            RemoveQueuedPlayer (itr->second);
            delete itr->second;
            m_sessions.erase(itr);
        }
    }
}

 

看一下WorldSession的Update()方法

bool WorldSession::Update(uint32 )
{
    ///- Retrieve packets from the receive queue and call the appropriate handlers
    /// not proccess packets if socket already closed
    WorldPacket* packet;  // 从接受队列中获取每个包,并调用相应的处理函数
    while (_recvQueue.next(packet) && m_Socket && !m_Socket->IsClosed ())
    {
       

        OpcodeHandler& opHandle = opcodeTable[packet->GetOpcode()]; //根据操作码获取处理函数
        try
        {
            switch (opHandle.status)
            {
                case STATUS_LOGGEDIN:
                    if(!_player)
                    {
                        // skip STATUS_LOGGEDIN opcode unexpected errors if player logout sometime ago - this can be network lag delayed packets
                        if(!m_playerRecentlyLogout)
                            LogUnexpectedOpcode(packet, "the player has not logged in yet");
                    }
                    else if(_player->IsInWorld())
                    {
                        (this->*opHandle.handler)(*packet);
                        if (sLog.IsOutDebug() && packet->rpos() < packet->wpos())
                            LogUnprocessedTail(packet);
                    }
                    // lag can cause STATUS_LOGGEDIN opcodes to arrive after the player started a transfer
                    break;
                case STATUS_LOGGEDIN_OR_RECENTLY_LOGGEDOUT:
                    if(!_player && !m_playerRecentlyLogout)
                    {
                        LogUnexpectedOpcode(packet, "the player has not logged in yet and not recently logout");
                    }
                    else
                    {
                        // not expected _player or must checked in packet hanlder
                        (this->*opHandle.handler)(*packet);
                        if (sLog.IsOutDebug() && packet->rpos() < packet->wpos())
                            LogUnprocessedTail(packet);
                    }
                    break;
                case STATUS_TRANSFER:
                    if(!_player)
                        LogUnexpectedOpcode(packet, "the player has not logged in yet");
                    else if(_player->IsInWorld())
                        LogUnexpectedOpcode(packet, "the player is still in world");
                    else
                    {
                        (this->*opHandle.handler)(*packet);
                        if (sLog.IsOutDebug() && packet->rpos() < packet->wpos())
                            LogUnprocessedTail(packet);
                    }
                    break;
                case STATUS_AUTHED:
                    // prevent cheating with skip queue wait
                    if(m_inQueue)
                    {
                        LogUnexpectedOpcode(packet, "the player not pass queue yet");
                        break;
                    }

                    // single from authed time opcodes send in to after logout time
                    // and before other STATUS_LOGGEDIN_OR_RECENTLY_LOGGOUT opcodes.
                    if (packet->GetOpcode() != CMSG_SET_ACTIVE_VOICE_CHANNEL)
                        m_playerRecentlyLogout = false;

                    (this->*opHandle.handler)(*packet);
                    if (sLog.IsOutDebug() && packet->rpos() < packet->wpos())
                        LogUnprocessedTail(packet);
                    break;
                case STATUS_NEVER:
                    sLog.outError( "SESSION: received not allowed opcode %s (0x%.4X)",
                        LookupOpcodeName(packet->GetOpcode()),
                        packet->GetOpcode());
                    break;
                case STATUS_UNHANDLED:
                    sLog.outDebug("SESSION: received not handled opcode %s (0x%.4X)",
                        LookupOpcodeName(packet->GetOpcode()),
                        packet->GetOpcode());
                    break;
                default:
                    sLog.outError("SESSION: received wrong-status-req opcode %s (0x%.4X)",
                        LookupOpcodeName(packet->GetOpcode()),
                        packet->GetOpcode());
                    break;
            }
        }
        catch(ByteBufferException &)
        {
            sLog.outError("WorldSession::Update ByteBufferException occured while parsing a packet (opcode: %u) from client %s, accountid=%i. Skipped packet.",
                    packet->GetOpcode(), GetRemoteAddress().c_str(), GetAccountId());
            if(sLog.IsOutDebug())
            {
                sLog.outDebug("Dumping error causing packet:");
                packet->hexlike();
            }
        }

        delete packet;
    }

    ///- Cleanup socket pointer if need
    if (m_Socket && m_Socket->IsClosed ())
    {
        m_Socket->RemoveReference ();
        m_Socket = NULL;
    }

    ///- If necessary, log the player out
    time_t currTime = time(NULL);
    if (!m_Socket || (ShouldLogOut(currTime) && !m_playerLoading))
        LogoutPlayer(true);

    if (!m_Socket)
        return false;                                       //Will remove this session from the world session map

    return true;
}

 

接受消息队列的数据包是哪来的呢? 看这里:

int WorldSocket::ProcessIncoming (WorldPacket* new_pct)
{
    ................

    try
    {
        switch(opcode)
        {
            case CMSG_PING:
                return HandlePing (*new_pct);
            case CMSG_AUTH_SESSION:
                if (m_Session)
                {
                    sLog.outError ("WorldSocket::ProcessIncoming: Player send CMSG_AUTH_SESSION again");
                    return -1;
                }

                return HandleAuthSession(*new_pct);
            case CMSG_KEEP_ALIVE:
                DEBUG_LOG ("CMSG_KEEP_ALIVE ,size: %d", new_pct->size ());

                return 0;
            default:
            {

                //若不是CMSG_PING,CMSG_AUTH_SESSION,CMSG_KEEP_ALIVE这几个消息则将消息队列起来
                ACE_GUARD_RETURN (LockType, Guard, m_SessionLock, -1);

                if (m_Session != NULL)
                {
                    // OK ,give the packet to WorldSession
                    aptr.release ();
                    // WARNINIG here we call it with locks held.
                    // Its possible to cause deadlock if QueuePacket calls back
                    m_Session->QueuePacket (new_pct);
                    return 0;
                }

 

Mangos之Socket处理方式

(2011-01-21 08:54:37)
转载
标签:

ace

mangos

socket

反应器

分类: Mangos代码阅读

先来看一下启动监听socket的地方:

 

Master.cpp   about line 316:

 

    ///- Launch the world listener socket
    port_t wsport = sWorld.getConfig (CONFIG_PORT_WORLD);
    std::string bind_ip = sConfig.GetStringDefault ("BindIP", "0.0.0.0");

    if (sWorldSocketMgr->StartNetwork (wsport, bind_ip.c_str ()) == -1)
    {
        sLog.outError ("Failed to start network");
        World::StopNow(ERROR_EXIT_CODE);
        // go down and shutdown the server
    }

    sWorldSocketMgr->Wait ();

 

Mangos用WorldSocketMgr这个类来管理socket的运作。StartNetwork中主要调用了StartReactiveIO来启动监听socket,看一下StartReactiveIO这个方法:

 

WorldSocketMgr::StartReactiveIO (ACE_UINT16 port, const char* address)
{
    m_UseNoDelay = sConfig.GetBoolDefault ("Network.TcpNodelay", true);

    int num_threads = sConfig.GetIntDefault ("Network.Threads", 1);//用于处理socket的线程个数

    if (num_threads <= 0)
    {
        sLog.outError ("Network.Threads is wrong in your config file");
        return -1;
    }

    m_NetThreadsCount = static_cast<size_t> (num_threads + 1);

    m_NetThreads = new ReactorRunnable[m_NetThreadsCount]; //ReactorRunnable继承ACE_Task_Base,这是ACE线程使用方法

    sLog.outBasic ("Max allowed socket connections %d",ACE::max_handles ());

    // -1 means use default
    m_SockOutKBuff = sConfig.GetIntDefault ("Network.OutKBuff", -1);

    m_SockOutUBuff = sConfig.GetIntDefault ("Network.OutUBuff", 65536);

    if ( m_SockOutUBuff <= 0 )
    {
        sLog.outError ("Network.OutUBuff is wrong in your config file");
        return -1;
    }

    WorldSocket::Acceptor *acc = new WorldSocket::Acceptor; // WorldSocket::Acceptor是 ACE_Acceptor< WorldSocket, ACE_SOCK_ACCEPTOR >类型,ACE的Acceptor-Connector Framework,WorldSocket是ACE_Svc_Handler,ACE_SOCK_ACCEPTOR macro帮助编译相应平台的ACE_Acceptor类


    m_Acceptor = acc;

    ACE_INET_Addr listen_addr (port, address);

    if (acc->open (listen_addr, m_NetThreads[0].GetReactor (), ACE_NONBLOCK) == -1)//指定监听地址,端口和第一个线程的反应器,并启动acceptor
    {
        sLog.outError ("Failed to open acceptor ,check if the port is free");
        return -1;
    }

    for (size_t i = 0; i < m_NetThreadsCount; ++i)
        m_NetThreads[i].Start ();//启动所有线程

    return 0;
}

每个线程都有一个反应器ACE_Reactor* m_Reactor,它的具体实现是ACE_TP_Reactor(用于多线程的reactor),

线程运行部分就是调用反应器来实现的:

 

        virtual int svc ()
        {
            DEBUG_LOG ("Network Thread Starting");

            WorldDatabase.ThreadStart ();  //?

            ACE_ASSERT (m_Reactor);

            SocketSet::iterator i, t;

            while (!m_Reactor->reactor_event_loop_done ())
            {
                // dont be too smart to move this outside the loop
                // the run_reactor_event_loop will modify interval
                ACE_Time_Value interval (0, 10000);

                if (m_Reactor->run_reactor_event_loop (interval) == -1)//运行10000微妙,run_reactor_event_loop本身会处理输入输出事件,只要svc_handler在reactor上注册了write,read事件。
                    break;

                AddNewSockets ();//将m_NewSockets的socket添加到m_Sockets中

                for (i = m_Sockets.begin (); i != m_Sockets.end ();)
                {
                    if ((*i)->Update () == -1)//m_Sockets是std::set<WorldSocket*>类型,这里循环每个WorldSocket,并调用其Update()方法,他只调用handle_output方法.

                    {
                        t = i;
                        ++i;
                        (*t)->CloseSocket ();
                        (*t)->RemoveReference ();
                        --m_Connections;
                        m_Sockets.erase (t);
                    }
                    else
                        ++i;
                }
            }

            WorldDatabase.ThreadEnd ();//线程将死

            DEBUG_LOG ("Network Thread Exitting");

            return 0;
        }

客户端连接产生的socket是怎样被分配到指定线程及相应的反应器的呢?

前面已经提到过,acceptor被指定用第一个线程和相应的反应器,当有客户端连接时,acceptor将创建一个svc_handler,这里是WorldSocket,然后调用WorldSocket的open方法。下面是图示

Mangos之Socket处理方式

在open方法中,调用了

    // Hook for the manager.
    if (sWorldSocketMgr->OnSocketOpen (this) == -1)
        return -1;

 

OnSocketOpen方法:

int
WorldSocketMgr::OnSocketOpen (WorldSocket* sock)
{
    // set some options here
    if (m_SockOutKBuff >= 0)
    {
        if (sock->peer ().set_option (SOL_SOCKET,
            SO_SNDBUF,
            (void*) & m_SockOutKBuff,
            sizeof (int)) == -1 && errno != ENOTSUP)
        {
            sLog.outError ("WorldSocketMgr::OnSocketOpen set_option SO_SNDBUF");
            return -1;
        }
    }

    static const int ndoption = 1;

    // Set TCP_NODELAY.
    if (m_UseNoDelay)
    {
        if (sock->peer ().set_option (ACE_IPPROTO_TCP,
            TCP_NODELAY,
            (void*)&ndoption,
            sizeof (int)) == -1)
        {
            sLog.outError ("WorldSocketMgr::OnSocketOpen: peer ().set_option TCP_NODELAY errno = %s", ACE_OS::strerror (errno));
            return -1;
        }
    }

    sock->m_OutBufferSize = static_cast<size_t> (m_SockOutUBuff);

    // we skip the Acceptor Thread
    size_t min = 1;

    ACE_ASSERT (m_NetThreadsCount >= 1);

    for (size_t i = 1; i < m_NetThreadsCount; ++i)
        if (m_NetThreads[i].Connections () < m_NetThreads[min].Connections ()) 
            min = i;

    return m_NetThreads[min].AddSocket (sock); //均衡将worldSocket分配给某个线程
}

AddSocket将worldSocket添加到m_NewSockets中。m_NewSockets在线程运行体中被添加到m_Sockets中,并调用其update()方法.

 

 int WorldSocket::Update (void)
{
    if (closing_)
        return -1;

    if (m_OutActive || (m_OutBuffer->length () == 0 && msg_queue()->is_empty()))//当m_OutBuffer有数据时才调用handle_output,这里是启动输出的地方,如果输出不能在handle_output中一次性处理完,则会调用schedule_wakeup_output让reactor继续处理输出数据,当handle_output中输出处理完毕之后则调用cancel_wakeup_output,reactor不能再处理输出数据,只用等到再次Update被调用,并且m_OutBuffer有数据是才再次启动输出。

        return 0;

    int ret;
    do
        ret = handle_output (get_handle ());
    while( ret > 0 );// ret>0 指write event还存在,就是还有数据要发送

    return ret;
}

 

Update主要调用handle_output方法

int WorldSocket::handle_output (ACE_HANDLE)
{
    ACE_GUARD_RETURN (LockType, Guard, m_OutBufferLock, -1); // m_OutBuffer是竞争资源需要保护

    if (closing_)
        return -1;

    const size_t send_len = m_OutBuffer->length ();

    if (send_len == 0)
        return handle_output_queue (Guard);

#ifdef MSG_NOSIGNAL
    ssize_t n = peer ().send (m_OutBuffer->rd_ptr (), send_len, MSG_NOSIGNAL);
#else
    ssize_t n = peer ().send (m_OutBuffer->rd_ptr (), send_len);
#endif // MSG_NOSIGNAL

    if (n == 0)
        return -1; 
    else if (n == -1)
    {
        if (errno == EWOULDBLOCK || errno == EAGAIN) //在多线程环境中可能其他线程已经处理了发送数据,EWOULDBLOCK表示暂时不能发送数据.
            return schedule_wakeup_output (Guard);

        return -1;
    }
    else if (n < send_len) //now n > 0 //发送了一部分数据,得继续发送
    {
        m_OutBuffer->rd_ptr (static_cast<size_t> (n));

        // move the data to the base of the buffer
        m_OutBuffer->crunch ();

        return schedule_wakeup_output (Guard);//启用后run_reactor_event_loop也能响应输出事件
    }
    else //now n == send_len //发送完毕则发送queue里的数据
    {
        m_OutBuffer->reset ();

        return handle_output_queue (Guard); //handle_output_queue中如果没有输出数据了则调用cancel_wakeup_output,之后run_reactor_event_loop不能响应输出事件.
    }

    ACE_NOTREACHED (return 0);
}

 

注意事项: 

使用ace的svc_handler时注意,注册read事件之后,reactor会根据实际网络输入来处理输入事件,而write事件要用户自己触发。


 

reamld中socket的处理方式

(2011-01-06 17:42:12)
转载
标签:

mangos

realmd

套接字

select方法

处理海量连接

分类: Mangos代码阅读

先来看一下reamld工程下的Main.cpp, 有如下内容

 

    SocketHandler h;    
    ListenSocket<AuthSocket> authListenSocket(h);
    if ( authListenSocket.Bind(bind_ip.c_str(),rmport))
    {
        sLog.outError( "MaNGOS realmd can not bind to %s:%d",bind_ip.c_str(), rmport );
        return 1;
    }

 

    h.Add(&authListenSocket);

 

    while (!stopEvent)
    {

        h.Select(0, 100000);   //100000 microseconds(一百万分之一秒) 作为select操作的超时时间

    。。。。。。。。。。。

 

SocketHandler,AuthSocket,ListenSocket这三个类几乎构成了reamld socket通信的全部。

ListenSocket用于监听来自客户端的连接请求,并把为每个创建的套接字传给AuthSocket

AuthSocket处理单个连接之间的消息,其实就是用来验证用户身份,返回游戏服务器地址列表等。

 

AuthSocket,ListenSocket都直接或间接继承自Socket这个基类,其封装了如SOCKET句柄等底层socket api的数据和函数。但整个控制流程又是怎样设计的呢? 比如accept一个连接请求,用这个连接创建的新套接字发送接收数据等等。

 

SocketHandler便是用于处理这整个控制流程的。它包含所有活跃的套接字,循环这些套接字select方法查看套接字的状态,如果为可读或可写则利用多态调用相应的读取和发送数据的具体方法。

 

在SocketHandler中,定义了以下集合

 
 typedef std::map<SOCKET,Socket *> socket_m;

 

 socket_m m_sockets; ///< Active sockets map
 socket_m m_add; ///< Sockets to be added to sockets map
 std::list<Socket *> m_delete; ///< Sockets to be deleted (failed when Add)

 

程序在需要的地方调用 SocketHandler::Add(Socket *p)将添加m_add集合,

每次调用SocketHandler::Select(struct timeval *tsel) 这个方法时,它会先将m_add中的sockets添加到

m_sockets中去并为这些新加入的sockets设置m_rfds,m_wfds,m_efds集合。

然后调用 n = select( (int)(m_maxsock + 1),&rfds,&wfds,&efds,tsel);  

 

现在可以检查某个集合是否有I/O操作,若有则调用回调函数,比如:

  for (socket_v::iterator it2 = m_fds.begin(); it2 != m_fds.end() && n; it2++) //遍历所有套接字
  {
   SOCKET i = *it2;
   if (FD_ISSET(i, &rfds)) //若在rfds集合内表示可读
   {
    socket_m::iterator itmp = m_sockets.find(i);
    if (itmp != m_sockets.end()) // found
    {
     Socket *p = itmp -> second;
     // new SSL negotiate method
#ifdef HAVE_OPENSSL
     if (p -> IsSSLNegotiate())
     {
      p -> SSLNegotiate();
     }
     else
#endif
     {
      p -> OnRead(); //调用回调函数,它具体可能是AuthSocket,ListenSocket的OnRead()方法
     }
    }

也就是说当ListenSocket调用OnRead()表示需要accept一个新的连接,AuthSocket调用OnRead()表示有数据从客户端传过来了。

 

ListenSocket的OnRead()方法:

ListenSocket

 
 void OnRead()
 {
  struct sockaddr sa;
  socklen_t sa_len = sizeof(struct sockaddr);
  SOCKET a_s = accept(GetSocket(), &sa, &sa_len);

  Socket *tmp = m_bHasCreate ? m_creator -> Create() : new X(Handler());

 

ListenSocket是template class,它接收X template参数,X这里是AuthSocket,作为服务具体某个连接的数据交换的类。可以看到ListenSocket将接收的新连接套接字付给了X.

 

  tmp -> SetIpv6( IsIpv6() );
#endif
  tmp -> SetParent(this);
  tmp -> Attach(a_s);

 

AuthSocket的OnRead()方法

/// Read the packet from the client
void AuthSocket::OnRead()
{
    ///- Read the packet
    TcpSocket::OnRead();
    uint8 _cmd;
    while (1)
    {
        if (!ibuf.GetLength())
            return;

        ///- Get the command out of it
        ibuf.SoftRead((char *)&_cmd, 1);                    // UQ1: No longer exists in new net code ???

        size_t i;

        ///- Circle through known commands and call the correct command handler
        for (i = 0; i < AUTH_TOTAL_COMMANDS; ++i)
        {
            if ((uint8)table[i].cmd == _cmd &&
                (table[i].status == STATUS_CONNECTED ||
                (_authed && table[i].status == STATUS_AUTHED)))
            {
                DEBUG_LOG("[Auth] got data for cmd %u ibuf length %u", (uint32)_cmd, ibuf.GetLength());

                if (!(*this.*table[i].handler)())

 

 AuthSocket继承TcpSocket,TcpSocket被设计用来处理基于连接的数据发送和接受任务,当调用    TcpSocket::OnRead()后,TcpSocket会用receive方法接收客户端传来的数据,并放置到一个CircularBuffer中ibuf。AuthSocket之后便调用ibuf.SoftRead((char *)&_cmd, 1)获取指令代码,如果是有效的指令码则继处理这个指令。         

 

补充知识点:

sock的I/O模式有两种,阻塞和非阻塞。在阻塞模式下,I/O操作完成前,执行操作的函数会一直等待下去,不会立即返回,这意味着应用程序很难同时通过多个建立好连接的套接字进行通行,这并不适合服务器的sock编程。在非阻塞模式下,I/O操作无论如何都会返回并交出程序的控制权。我们可以ioctlsocket函数来设置套接字的I/O模式,如:

SOCKET s = socket(AF_INET,SOCK_STREAM,0);

int nStatus = ioctlsocket(s,FIOBIO,&cmd);

当套接字被设为非阻塞后, I/O调用会立即返回,但大多情况下会调用“失败”,返回一个WOULDBLOCK错误表示请求操作在调用期间没有时间完成。所以需要通过不断检查函数返回代码以判断一个套接字何时可供读写。为了免去这样的麻烦,sock api提供了Select模型对I/O进行管理。(其他还有AsyncSelect,EventSelect,Overlapped, Completion port等)

 

Select模型是最为常见的I/O模型。通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否存在数据,或则能否向一个套接字写入数据。select函数的原型:

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout);

其中,ndfs可以忽略,只是起到兼容的作用,readfds、writefds、exceptfds三个fd_set数据类型的参数分别指向等待可读检查的套接字组、等待可写检查的套件字组、等待错误检查的套接字组的指针。timeout指定select()最多等待的时间。

当select返回,如果调用成功,则可以判断套接字是否仍为readfds,writefds集合的一员,若是则表示套接字可读或可写

if(FD_ISSET(s,&fdread){

   //从套接字中读取数据

}

 

Mangosd消息的对称加密解析

(2011-01-05 09:44:46)
转载
标签:

mangos

rc4

hmac

对称加密

解析

分类: Mangos代码阅读

当服务器Mangosd验证完客户端身份,之后从服务器向客户端发送第一个消息(SMSG_AUTH_RESPONSE)开始,便一直用RC4对消息包头进行加密。客户端接收到消息后必须先进行解密,才能获得有效数据以便继续往下处理。

 

服务端是对包头信息加密,包头的定义如下:

struct ServerPktHeader
{
   
    ServerPktHeader(uint32 size, uint16 cmd) : size(size)
    {
        uint8 headerIndex=0;
        if(isLargePacket())
        {
            sLog.outDebug("initializing large server to client packet. Size: %u, cmd: %u", size, cmd);
            header[headerIndex++] = 0x80|(0xFF &(size>>16));
        }
        header[headerIndex++] = 0xFF &(size>>8);
        header[headerIndex++] = 0xFF &size;

        header[headerIndex++] = 0xFF & cmd;
        header[headerIndex++] = 0xFF & (cmd>>8);
    }

    uint8 getHeaderLength()
    {
        // cmd = 2 bytes, size= 2||3bytes
        return 2+(isLargePacket()?3:2);
    }

    bool isLargePacket()
    {
        return size > 0x7FFF;
    }

    const uint32 size;
    uint8 header[5];
};

包头可能包含4个或5个字节,分别对应小数据和大数据。

开始2或3字指数据包长度size,总是以大端存放不管具体操作系统如何存放数据的。

接着两位是消息指令代码cmd,对应某个类型的消息比如SMSG_AUTH_RESPONSE。

 

当服务器放送消息时先将包头加了密

int WorldSocket::SendPacket (const WorldPacket& pct)
{

    。。。。。

    ServerPktHeader header(pct.size()+2, pct.GetOpcode());
    m_Crypt.EncryptSend ((uint8*)header.header, header.getHeaderLength());

 

m_Crypt是专门用来处理对称加密解密的成员,它在处理CMSG_AUTH_SESSION时被初始化

int WorldSocket::HandleAuthSession (WorldPacket& recvPacket)『

    。。。。。。

    m_Crypt.Init(&K); //用K(sessionKey)初始化

 

来看一下这个初始化函数

void AuthCrypt::Init(BigNumber *K)
{
    uint8 ServerEncryptionKey[SEED_KEY_SIZE] = { 0x22, 0xBE, 0xE5, 0xCF, 0xBB, 0x07, 0x64, 0xD9, 0x00, 0x45, 0x1B, 0xD0, 0x24, 0xB8, 0xD5, 0x45 };
    HmacHash serverEncryptHmac(SEED_KEY_SIZE, (uint8*)ServerEncryptionKey);
    uint8 *encryptHash = serverEncryptHmac.ComputeHash(K);

    uint8 ServerDecryptionKey[SEED_KEY_SIZE] = { 0xF4, 0x66, 0x31, 0x59, 0xFC, 0x83, 0x6E, 0x31, 0x31, 0x02, 0x51, 0xD5, 0x44, 0x31, 0x67, 0x98 };
    HmacHash clientDecryptHmac(SEED_KEY_SIZE, (uint8*)ServerDecryptionKey);
    uint8 *decryptHash = clientDecryptHmac.ComputeHash(K);

    //SARC4 _serverDecrypt(encryptHash);
    _clientDecrypt.Init(decryptHash);
    _serverEncrypt.Init(encryptHash);
    //SARC4 _clientEncrypt(decryptHash);

    uint8 syncBuf[1024];

    memset(syncBuf, 0, 1024);

    _serverEncrypt.UpdateData(1024, syncBuf);
    //_clientEncrypt.UpdateData(1024, syncBuf);

    memset(syncBuf, 0, 1024);

    //_serverDecrypt.UpdateData(1024, syncBuf);
    _clientDecrypt.UpdateData(1024, syncBuf);

    _initialized = true;
}

初始化函数先用ServerEncryptionKey,K做HmacHash运算,获得的摘要结果作为rc4加密的seed

_serverEncrypt是SARC实例,起初化操作中做了下面操作

    EVP_EncryptInit_ex(&m_ctx, EVP_rc4(), NULL, NULL, NULL);
    EVP_EncryptInit_ex(&m_ctx, NULL, NULL, seed, NULL); //seed用的是encryptHash
初始化完后,当服务器要加密包头时调用

    _serverEncrypt.UpdateData(len, data); //len为加密的字节数,data为要加密的字节数组

这个操作后data发生了改变,加了密,服务器将消息(加密包头+内容)发送到客户端

 

 

客户端解密

由于消息加了密,客户端不能直接使用接收到的消息,我们知道服务器是用rc4并用sessionkey和ServerEncryptionKey的HmacHash摘要结果作为rc4算法的seed.

rc4是对称加密算法,在客户端我们只需将服务器对包头做的操作重复一遍及是解密操作了。

在客户端我们有相应sessionkey,而ServerEncryptionKey是hardcode的。

 

  char szRecvBuffer4[120];
  int iBytesReceived4 = recv(sockGame,szRecvBuffer4,120,0);
  cout << "reponse from serv" <<endl;

  SARC4 _serverDecrypt;
  uint8 ServerDecryptionKey[SEED_KEY_SIZE] = { 0x22, 0xBE, 0xE5, 0xCF, 0xBB, 0x07, 0x64, 0xD9, 0x00, 0x45, 0x1B, 0xD0, 0x24, 0xB8, 0xD5, 0x45 };
  HmacHash serverDecryptHmac(SEED_KEY_SIZE, (uint8*)ServerDecryptionKey);
  uint8 *decryptHash = serverDecryptHmac.ComputeHash(&K);
  _serverDecrypt.Init(decryptHash);

  uint8 syncBuf[1024];
  memset(syncBuf, 0, 1024);
  _serverDecrypt.UpdateData(1024, syncBuf);

  //重复服务器做的事情便是解密了

  _serverDecrypt.UpdateData(4, (uint8*)szRecvBuffer4);

  uint16 pkSize = parseSize(szRecvBuffer4);
  uint16 cmd = *((uint16*)&szRecvBuffer4[2]);

 

补充知识:

HAMC是密钥相关的哈希运算消息认证码(keyed-Hash Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出.

 

RC4加密算法是Ron Rivest在1987年设计的密钥长度可变的流加密算法簇;RC4得到密文,解密过程完全相同.

 

详解验证Mangos服务器:消息SMSG_AUTH_CHALLENGE,CMSG_AUTH_SESSION,SMSG_AUTH_RESPONSE

(2011-01-04 11:49:17)
转载
标签:

mangos

验证

消息

sha1加密

分类: Mangos代码阅读

当在wow输入帐号密码登录后,客户端程序先连接的是登录服务器,(这里是叫realmd.exe的进程)通过sha1密文验证流程后,获取游戏服务器的ip,port,(这里便是mangosd.exe进程对应的ip,port)客户端接着连接这个游戏服务器。

 

这里阐述的是连接到mangos服务器后的验证过程.

 

1。 当与mangos服务器建立tcp连接后,服务器会想客户端发送消息SMSG_AUTH_CHALLENGE

 

int WorldSocket::open (void *a)
{

    。。。。

    // Send startup packet.
    WorldPacket packet (SMSG_AUTH_CHALLENGE, 24);
    packet << uint32(1);                                    // 1...31
    packet << m_Seed;
    packet << uint32(0xF3539DA3);                           // random data
    packet << uint32(0x6E8547B9);                           // random data
    packet << uint32(0x9A6AA2F8);                           // random data
    packet << uint32(0xA4F170F4);                           // random data

    if (SendPacket (packet) == -1)

    。。。。。。。

 

这里实际要用的信息为SMSG_AUTH_CHALLENGE这个OpCode,m_Seed一个随机数。

 

2.客户端获取这个OpCode为SMSG_AUTH_CHALLENGE的消息后,知道服务器要求客户端提供身份验证信息。

于是在客户端构造消息CMSG_AUTH_SESSION:

  //do auth
  BigNumber clientSeed;
  clientSeed.SetRand(4 * 8);
  sha.Initialize();
  sha.UpdateData ("ADMINISTRATOR");
  uint32 t = 0;
  sha.UpdateData ((uint8 *) & t, 4);
  sha.UpdateBigNumbers(&clientSeed, NULL);
  sha.UpdateData ((uint8 *) & serverSeed, 4);
  sha.UpdateBigNumbers (&K, NULL);
  sha.Finalize();

  uint32 unk2, unk3;
  uint64 unk4;

  ByteBuffer pkt3;
  string account = "ADMINISTRATOR";
  uint16 pkt3DataSize = 4+4+4+account.length()+1+4+4+8+20;
  EndianConvertReverse(pkt3DataSize);
  pkt3 << uint16(pkt3DataSize);
  pkt3 << uint32(CMSG_AUTH_SESSION); //opCode
  pkt3 << uint32(12484); //build version
  pkt3 << unk2;
  pkt3 << account;
  pkt3 << unk3;
  pkt3.append(clientSeed.AsByteArray(4),4);
  pkt3 << unk4;
  pkt3.append(sha.GetDigest(), 20);

  send(sockGame,(char const*)pkt3.contents(), pkt3.size(), 0);
  cout << "send AuthRequest" <<endl;

 

这里关键要构造sha验证密文:

20位密文 = sha(account, serverSeed,clientSeed,K)

account是帐号文本,serverSeed是SMSG_AUTH_CHALLENGE中传过来的服务端生成的随机数,clientSeed是相应的客户端生成的,K是之前验证登录服务器时生成的(客户端与服务端都有一份相同的K,服务端存放在realmd.account.sessionkey字段下)

客户端将account,clientSeed,sha密文传向服务端

 

3.在服务端处理客户端发送的消息

1)首先服务端读取包头大小的数据,验证包头信息

包头的结构:

struct ClientPktHeader
{
    uint16 size;  //包长,不算自身16位长度
    uint32 cmd;   //opCode
};

 

读取包长的数据

int WorldSocket::handle_input_missing_data (void){

。。。。。

            //need to receive the header
            const size_t to_header = (message_block.length () >m_Header.space () ? m_Header.space () : message_block.length ());
            m_Header.copy (message_block.rd_ptr (), to_header);
            message_block.rd_ptr (to_header);

。。。。。

 

m_Header是WorldSocket类的成员,定义:

        /// Fragment of the received header.
        ACE_Message_Block m_Header;

并在构造函数中初始化

WorldSocket::WorldSocket (void) :
WorldHandler (),
m_Session (0),
m_RecvWPct (0),
m_RecvPct (),
m_Header (sizeof (ClientPktHeader)),

。。。。

 

处理包头信息

int WorldSocket::handle_input_header (void)
{

    。。。。。。

    m_Crypt.DecryptRecv ((uint8*) m_Header.rd_ptr (), sizeof (ClientPktHeader));

    ClientPktHeader& header = *((ClientPktHeader*) m_Header.rd_ptr ());

    EndianConvertReverse(header.size);
    EndianConvert(header.cmd);

    。。。。。。

    ACE_NEW_RETURN (m_RecvWPct, WorldPacket ((uint16) header.cmd, header.size), -1);

    if(header.size > 0)
    {
        m_RecvWPct->resize (header.size);
        m_RecvPct.base ((char*) m_RecvWPct->contents (), m_RecvWPct->size ());
    }
    。。。。。。

 

这里调用EndianConvertReverse(header.size)是因为用16位表示的header.size是按正常相反的Endian存放的,

(如果系统是小端存放,则这个size是以大端存放)

m_RecvWPct用来存放客户端发送的数据包,m_RecvPct其实只向m_RecvWPct的同一块内存区域

 

2)处理数据包

首先读取包头中size长的内容

int WorldSocket::handle_input_missing_data (void)『

。。。。。。。。。

            //need more data in the payload
            const size_t to_data = (message_block.length () > m_RecvPct.space () ? m_RecvPct.space () : message_block.length ());
            m_RecvPct.copy (message_block.rd_ptr (), to_data);
            message_block.rd_ptr (to_data);
。。。。。。。。。。

处理数据包

int WorldSocket::handle_input_payload (void)『

。。。。。。。。

    const int ret = ProcessIncoming (m_RecvWPct);

。。。。。。。

 

int WorldSocket::ProcessIncoming (WorldPacket* new_pct)
{

    const ACE_UINT16 opcode = new_pct->GetOpcode ();

。。。。。。。。。。。。

        switch(opcode)
        {
            case CMSG_PING:
                return HandlePing (*new_pct);
            case CMSG_AUTH_SESSION:
                if (m_Session)
                {
                    sLog.outError ("WorldSocket::ProcessIncoming: Player send CMSG_AUTH_SESSION again");
                    return -1;
                }

                return HandleAuthSession (*new_pct);

。。。。。。。。。

 

int WorldSocket::HandleAuthSession (WorldPacket& recvPacket)『

。。。。。。。。。。。。。。。。

    Sha1Hash sha;

    uint32 t = 0;
    uint32 seed = m_Seed;

    sha.UpdateData (account);
    sha.UpdateData ((uint8 *) & t, 4);
    sha.UpdateData ((uint8 *) & clientSeed, 4);
    sha.UpdateData ((uint8 *) & seed, 4);
    sha.UpdateBigNumbers (&K, NULL);
    sha.Finalize ();

    if (memcmp (sha.GetDigest (), digest, 20))

。。。。。。。。。。。。。。。。

 

    // NOTE ATM the socket is single-threaded, have this in mind ...
    ACE_NEW_RETURN (m_Session, WorldSession (id, this, AccountTypes(security), expansion, mutetime, locale), -1);

    m_Crypt.Init(&K);

    m_Session->LoadGlobalAccountData();
    m_Session->LoadTutorialsData();
    m_Session->ReadAddonsInfo(recvPacket);

    // In case needed sometime the second arg is in microseconds 1 000 000 = 1 sec
    ACE_OS::sleep (ACE_Time_Value (0, 10000));

    sWorld.AddSession (m_Session);

 

在服务端以客户端同样方式计算sha密文, 并与客户端传来的比较,如果相同则验证成功,

接着创建WorldSession实例,并初始化m_Crypt对称加密成员,之后服务器发送的消息就是加密过的了。

添加到m_Session游戏世界对象中,这一添加动作将在游戏世界主线程中被得到相应处理。

 

3)处理WorldSession的添加,在游戏世界对象World中

void World::UpdateSessions( uint32 diff )
{
    ///- Add new sessions
    WorldSession* sess;
    while(addSessQueue.next(sess))
        AddSession_ (sess);

 

void
World::AddSession_ (WorldSession* s)
{

。。。。。。。。。。

一些关于PlayerCache的操作

如果Session能作为ActiveSession而不是QueuedSession(等待进入游戏)则

发送SMSG_AUTH_RESPONSE消息,其内容将被加密

    WorldPacket packet(SMSG_AUTH_RESPONSE, 1 + 4 + 1 + 4 + 1);
    packet << uint8 (AUTH_OK);
    packet << uint32 (0);                                   // BillingTimeRemaining
    packet << uint8 (0);                                    // BillingPlanFlags
    packet << uint32 (0);                                   // BillingTimeRested
    packet << uint8 (s->Expansion());                       // 0 - normal, 1 - TBC, must be set in database manually for each account
    s->SendPacket (&packet);

4. 消息样例

 

connect to 192.168.111.128 8085

<=192.168.111.128 8085(ACK PUSH) SMSG_AUTH_CHALLENGE
00 1A | EC 01 01 00 | 00 00 57 B9 7B 6C A3 9D | 53 F3 B9 47 | 85 6E F8 A2 | 6A 9A F4 70
F1 A4

=>192.168.111.128 8085(ACK PUSH) CMSG_AUTH_SESSION
01 16 | ED 01 00 00 | C4 30 00 00 00 00 00 00 | 41 44 4D 49 | 4E 49 53 54 | 52 41 54 4F
52 00 00 00 | 00 00 9B DD | 02 04 01 00 | 00 00 00 00
00 00 E0 AB | FE BA B0 81 | 86 C3 32 8E | B5 F9 65 42
20 2B 7C 12 | DE 76 9E 02 | 00 00 78 9C | 75 D2 31 6E
C3 30 0C 05 | 50 F5 14 5D | 72 99 3A 01 | 0C 23 D1 52
2B 73 41 4B | BF 36 61 89 | 32 64 39 4D | 72 9D 5E B4
E8 D6 02 F4 | FC 88 4F E2 | 83 AF C6 98 | 26 F2 F3 49
25 7C BC F9 | 89 71 43 82 | D4 6B 67 5E | D2 D7 E1 62
FE 79 81 90 | 2E 9B AF 9C | 45 B5 86 CA | 80 B2 4E 79
D9 E1 5A 23 | 3E 19 31 58 | 16 4E B4 68 | 43 2C 81 65
54 03 8E 14 | 21 81 8A 46 | 39 0D 54 2F | 79 DC 35 87
7B 55 F0 84 | 61 1B 5D CE | 71 55 B0 8D | 8F 65 52 4F
69 ED 71 22 | BD BB D6 F6 | 5B B9 E1 A1 | E3 C6 31 34
24 B3 AA 9D | AC 0B BC 1E | DB 55 A4 3E | FB 19 75 AF
1E 4B BE 64 | 55 DE 89 83 | 0A EE B7 51 | 7D 9F E3 04
4B 42 23 B4 | BE 5D 9E A1 | 3F 81 2B 14 | D0 CF 1C E3
1E B3 A0 FC | B5 F3 F7 E9 | FC 03 E1 91 | C8 AB

<=192.168.111.128 8085(ACK PUSH) (encrypted)
85 57 | 9E 3C 0C 00 | 00 00 00 00 00 00 00 00 | 00 24 FC 8A | 06 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 02 01 00 | 00 00 00 00 | 00 02 01 00 | 00 00 00 00
00 00 00 00 | 00 E1 6C F5 | 24 00 00 00 | 00 5C 62 2D
24 17 C6 17 | 4D 01 15 00 | 00 00 80 C5 | 17 4D 00 00
00 00 81 C5 | 17 4D 38 D5 | 2C B0 00 00 | 00 00 00 00
00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00
00 00 00 00 | 00 00 00 00 | 00 00

 

Mangos游戏世界主线程解析

(2011-01-04 09:07:24)
转载
标签:

mangos

游戏世界

主线程

分类: Mangos代码阅读

先列一下主要相关文件名称及位置:

 

游戏世界定义文件

..\mangos\src\game\World.h 及 World.cpp

在类World中通过加载*.dbc,*.map,配置文件来构造游戏世界,其中包括游戏数据,设置项等。

 

线程基类文件

..\mangos\src\shared\Threading.h 及 Threading.cpp

在该文件中定义的类Runnable,Thread, ThreadPriority

Runnable是个接口,通过实现方法run()来定义线程实际要做的事情

Thread, ThreadPriority说白了就是对ACE线程API的Wrapper.

 

运行游戏世界的线程文件

..\mangos\src\mangosd\WorldRunnable.h 及 WorldRunnable.cpp

WorldRunnable顾名思义实现了Runnable接口,定义游戏世界线程循环要做的事情

其中关键是调用了World实例sWorld的Update()方法,大约在50~60行的位置:

    while (!World::IsStopped())
    {
        ++World::m_worldLoopCounter;
        realCurrTime = getMSTime();

        uint32 diff = getMSTimeDiff(realPrevTime,realCurrTime);

        sWorld.Update( diff );

        。。。。。。。。。。。。

 

最后看一下开启这个主线程的地方:

文件位置..\mangos\src\mangosd\Master.cpp,大约226行

    ///- Launch WorldRunnable thread
    ACE_Based::Thread world_thread(new WorldRunnable);
    world_thread.setPriority(ACE_Based::Highest);

 

上述只是摘要性地说明,一旦深入研究其中某个细节后,我会将内容相应添加进来。

 

 

 

 

 


 

 

原创粉丝点击