在图书馆发现一本《网络多人游戏架构与编程》—— Joshua Glazer, Sanjay Madhav 著。书挺新的,17年出版的,内容很有趣,翻一翻可以学到不少在《计算机网络》上不会讲到的内容,故做此纪录。
前几章,第一章简单介绍了网络游戏的历史和发展,第二章讲了how Internet works, 第三章讲的是 Berkeley Socket,就略过了。
对象序列化
序列化:是指把内存中的内容转化为比特流的形式。比特流是通过网络传输的形式,在主机和服务器上还可以恢复为原始格式。
可能不是很好理解,书中举一个例子来说,如果有一只猫 RoboCat 对象要传输,猫的类定义代码如下:
// 类定义
class RoboCat : public GameObject
{
public:
RoboCat() : mHealth(10), mMeowCount(3) {}
virtual void Update();
private:
int32_t mHealth;
int32_t mMeowCount;
GameObject * mHomeBase
char name[128];
std::vector<int32_t> mMiceIndices;
}
用socket发送这只猫的代码如下:
void SendRoboCat(int inSocket, const RoboCat * inRoboCat)
{
send(inSocket,
reinterpret_cast<const char *>(inRoboCat),
sizeof(RoboCat), 0);
}
乍一看好像是没有什么问题对不对?服务器那端用 recv 函数把这个对象接住就可以。问题在哪呢?比如看这一行:
virtual void Update();
假设在32位系统上,这个对象开始的 4 bytes 是一个虚函数表指针。因为 Update
方法是虚的,每个对象实例都会存储一个指向虚方法实现位置的表指针(如果看不懂自行搜索虚函数的实现方法)。如果直接发送这个对象,会导致一个问题,那就是每个不同进程中那张表的位置其实是不一样的。这段代码,会导致服务器上写入目标的那个 RoboCat 对象的虚函数表指针直接写成一个错的指针。
当然这个例子中还包含别的指针,比如 GameObject * mHomeBase
,结合上面的例子很容易就想通:在客户端上一个进程的某一个指针位置,直接发给服务器肯定是荒谬的(一个进程复制一个指针到另一个进程,肯定不能期望对这个指针解引用后还能得到正确的数据)。解决方案其实很容易就能想到,就是必须对相关的数据进行复制,而不是发送直接的二进制地址。
第二个问题就是对于char name[128];
这个对象,如果直接发送整个对象,很明显的一个问题就是这个128字节的数组大多数情况下包含的数据很少,因为这是一个以