关于operator new的一些理解,算是重读《Effective C++》有感

1. operator new的实现

# in gcc/libstdc++-v3/libsupc++/new_op.cc
....
#include <bits/c++config.h>
#include <cstdlib>
#include <bits/exception_defines.h>
#include "new"

using std::new_handler;
using std::bad_alloc;
#if _GLIBCXX_HOSTED
using std::malloc;
#else
// A freestanding C runtime may not provide "malloc" -- but there is no
// other reasonable way to implement "operator new".
extern "C" void *malloc (std::size_t);
#endif

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
    void *p;

    /* malloc (0) is unpredictable; avoid it.  */
    if (sz == 0)
        sz = 1;

    while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {
        new_handler handler = std::get_new_handler ();
        if (! handler)
        _GLIBCXX_THROW_OR_ABORT(bad_alloc());
        handler ();
    }

    return p;
}

2. 我们看到并需要理解的

2.1 handler函数是循环调用的

如果malloc没有成功,handler函数会循环调用,除非我们将handler设置为空,或者在handler中抛出异常。这样我们再去看条款49也就比较好理解了。

3. placement new

如果operator new接受的参数除了一定会有的那个size_t之外,还有其他的参数,这便是所谓的placement new。

# in gcc/libstdc++-v3/libsupc++/new
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }

说到placement new,最先想到的就是上面这个版本,输入一个内存区域指针。看到实现发现,如此简单,就直接返回了。

3.1 placement new / delete

如果实现了placement new,则需要实现对应的placement delete。因为如果类构造函数出异常了,c++内部会调用对应的placement delete。

4. 关于构造函数和析构函数出问题需要怎么处理

参考:https://isocpp.org/wiki/faq/exceptions#ctors-can-throw

在构造函数中,抛出异常;在析构函数中,中断程序。

总是反反复复在用socket api,但每次都要查询manual,接下来就总结一下这些api吧

基本的Socket API

socket

#incldue <sys/socket.h>
int socket(int domain, int type, int protocal);
                Return file descriptor on success, or -1 on error

domain:

  1. AF_INET用于IPv4, sockaddr_in, 32bit IPv4 address + 16bit port number
  2. AF_INET6用于IPv6,sockaddr_in6, 128bit IPv6 address + 16bit port number

type:

  1. SOCK_STREAM: TCP相关
  2. SOCK_DGRAM: UDP相关

protocal:
基本为0

bind

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
                        Return 0 on sucess, or -1 on error

关于struct sockaddr,其是一个通用的地址结构,sa_family用于指明是哪种类型(IPv4 or IPv6),sa_data就根据不同的结构进行调整

struct sockaddr {
    sa_family_t sa_family;   /* Address family (AF_* constant) */
    char sa_data[14];        /* Socket address (size varies according to socket domain) */
};

/* IPv4 address */
struct in_addr {                /* IPv4 4-byte address */
    in_addr_t s_addr;           /* Unsigned 32-bit integer */
};

struct sockaddr_in {            /* IPv4 socket address */
    sa_family_t    sin_family;  /* Address family (AF_INET) */
    in_port_t      sin_port;    /* Port number */
    struct in_addr sin_addr;    /* IPv4 address */
    unsigned char  __pad[X];    /* Pad to size of 'sockaddr' structure (16 bytes) */
};

listen

#include <sys/socket.h>
int listen(int sockfd, int backlog);
                     Return 0 on success, or -1 on error

关于backlog:简单说就是定义pending队列的长度

accept

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
                     Return file descriptor on success, or -1 on error

accept的三个参数就比较好理解了,accept后面两个参数是链接过来的ip和port

connect

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *addr, socklen_t addrlen);
                     Return 0 on success, or -1 on error

网络格式

网络传输都是大端传输的,所有我们需要对ip和port进行转换(转为大端)。
大端:高位在低地址
小端:高位在高地址

#include <arpa/inet.h>

uint16_t htons(uint16_t host_uint16);  /* host to net */
                     Return host_uint16 converted to network byte order
uint32_t htonl(uint32_t host_uint32);  /* host to net */
                     Return host_uint32 converted to network byte order
uint16_t ntohs(uint16_t net_uint16);   /* net to host */
                     Return net_uint16 coverted to host byte order
uint32_t ntohl(uint32_t net_uint32);   /* net to host */
                     Return net_uint32 converted to host byte order

inet_pton和inet_ntop函数

#include <arpa/inet.h>

int inet_pton(int domain, const char * src_str, void *addrptr);
                     Return 1 on successful conversion, 0 if src_str is not in 
                     presentation format, or -1 on error

const char *inet_ntop(int domain, const void* addrptr, char *dst_str, size_t len);
                     Returns pointer to dst_str on success, or NULL on error

inet_pton是将字符串转为in_addr或者in6_addr,因此两个函数的addrptr传入的就是上面这两个addr类型;另外第二个api的len参数是表示dst_str长度的

持续更新中…

今天看了一下Python源码,简单了解获取__dict__的流程,这里做一下简单的总结,为后续回头查看提供方便

Class的__dict__

先看一个例子:

> class A(object): pass
> ...
> A.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})

发现dict是一个dict_proxy类型,为何不是一个简单的python dict呢?跟一下代码看一下:

case LOAD_ATTR:
    w = GETITEM(names, oparg);
    v = TOP();
    x = PyObject_GetAttr(v, w);
    Py_DECREF(v);
    SET_TOP(x);
    if (x != NULL) continue;
    break;

获取变量会执行LOAD_ATTR的机器码,对于Class A会走到如下函数:

# in typeobject.c
static PyObject *
type_getattro(PyTypeObject *type, PyObject *name) {
    ...
    meta_attribute = _PyType_Lookup(metatype, name);  【1】
    if (meta_attribute != NULL) {
    meta_get = Py_TYPE(meta_attribute)->tp_descr_get;

    if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
        /* Data descriptors implement tp_descr_set to intercept
         * writes. Assume the attribute is not overridden in
         * type's tp_dict (and bases): call the descriptor now.
         */
        return meta_get(meta_attribute, (PyObject *)type,
                        (PyObject *)metatype);   【2】
    }

}

metatype:对于一个Class的Metatype是type,对于Class,会在type中寻找__dict__(参考1),返回一个描述符,并调用描述符get函数(参考2)。

最终会运行到下面的代码:

# in typeobject.c
static PyObject *
type_dict(PyTypeObject *type, void *context)
{
    if (type->tp_dict == NULL) {
        Py_INCREF(Py_None);
        return Py_None;
    }
    return PyDictProxy_New(type->tp_dict); 【1】
}

【1】这里type就是Class A,所有就是读取Class A的tp_dict

Instance的__dict__

读取一个类的实例的__dict__会调用到如下方法:

# in object.c
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict) {
    ....
    descr = _PyType_Lookup(tp, name);   【1】

}

这里tp就是Class A,因此我们需要在Class A的tpdict寻找\_dict__变量,ok,使用下面的方式看一下:

> A.__dict__["__dict__"]
<attribute '__dict__' of 'A' objects>
> type(A.__dict__["__dict__"])
<type 'getset_descriptor'>

这样会调用到对应的descriptor

# in typeobject.c
static PyObject *
subtype_dict(PyObject *obj, void *context) {
    ....
    dictptr = _PyObject_GetDictPtr(obj);
    ....
    dict = *dictptr;
    ....
    return dict;
}

上面的obj就是Class A的实例。

Module的__dict__

Module的基本逻辑与Instance相似,也是调用_PyObject_GenericGetAttrWithDict方法。

> type(a_p).__dict__['__dict__']
<member '__dict__' of 'module' objects>
> type(type(a_p).__dict__['__dict__'])
<type 'member_descriptor'>

会拿到module的__dict__,然后调用描述符对应的get函数。

# in structmember.c
PyObject *
PyMember_GetOne(const char *addr, PyMemberDef *l) 
....

前一阵子在项目中使用tcmalloc的heap-checker查找内存泄露的问题,今天翻译一下官方文档的一篇heap-checker相关的文章,由于时间有限,没有完全翻译完,只翻译了比较重要的部分,后续将会补全。

在Google,我们使用heap checker检查C++程序的内存泄露。使用heap checker分三步:将lib库链接到应用中;运行代码;分析输出结果。

链接tcmalloc

heap-checker是tcmalloc的一部分,为了在应用程序中安装heap checker,在程序链接阶段需要加入-ltcmalloc。但上面这种方法并不是唯一的方式,我们可以使用LD_PRELOAD宏在程序运行时加入tcmalloc:

% env LD_PRELOAD="/usr/lib/libtcmalloc.so"

上面这种方式并没有打开heap checking;仅仅将代码插入到程序中。因此,在开发阶段我们建议使用链接tcmalloc库的方法,这也是我们在Google使用的方法(然而,我们也可以使用环境变量开启profiler,因此通过链接tcmalloc库并不是必须的。)。需要注意的是为了使用heap checker必须使用tcmalloc内存分配库。

运行代码

注意:由于一些安全原因,对于setuid程序,heap profiler在使用的过程中并不会写文件。

对整个程序进行内存泄露检查

我们推荐使用heap-checker的方式是“全程序”模式。在这种模式下,heap-checker在main()函数开始之前跟踪内存分配,在程序介绍的时候再次检查。如果发现有内存泄露(任何已经分配的内存在程序结束的时候并没有对象引用它们),程序将会中断(通过exit(1))并且打印怎样跟踪内存泄露的信息(使用pprof)

heap-checker会记录每一次内存分配的调用栈,因此开启heap-checker会导致程序的内存增长并影响程序的性能。

下面介绍怎么开启全程序模式的内存泄露检查:

  1. 定义环境变量 HEAPCHECK 声明内存泄露检查的模式。例如下面的方式:

    % env HEAPCHECK=normal /usr/local/bin/my_binary_compiled_with_tcmalloc

没有其他的操作了。

需要注意的是heap-checker使用heap-profiling的架构,因此不可能同时运行heap-checker和heap-profiler。

内存检查的特性

如下是在使用全程序内存检查的合法变量:

  1. minimal
  2. normal
  3. strict
  4. draconian

“minimal”的模式开始内存泄露检查尽可能晚,意味着你可以在初始化例程存在一些内存泄露(在main()函数前),但并不会让heap-checker记录。如果你在全局初始化时候存在内存泄露,“minimal”模式就很适合。否则,你应该使用更加严格的模式。

“normal”模式跟踪存活对象(live objects)并报告其内存泄露信息(在程序结束时,无法通过存活对象找到的内存空间都是内存泄露)

“strict”模式与“normal”模式很相似,但“strict”模式会监控全局变量析构函数的内存泄露。例如,如果你有一个全局变量,在运行时申请了部分内存,在析构函数中忘记释放内存,在“strict”模式下将会被监控到,在“normal”模式下将不会。

“draconian”模式适合想清晰了解应用程序内存管理的情况,其希望heap-checker去帮助他们优化内存管理。在“draconian”模式,heap-checker并不会只检查存活对象,只要有内存泄露,其都会报告。

“normal”模式是最常用的模式。

as-is”是一个更加灵活的模式;它允许你自定义heap-checker的一些特性。“local”激活“显式heap-check指令”,但并不会开启任何全程序内存泄露检查。

用于调试内存泄露的小方法

工作原理

当一个HeapLeakChecker对象被构造的时候,它会在tmp文件夹输出一个名为.-beg.heap的记录内存使用信息的文件。当NoLeak()被调用的时候(对于全程序检查,其发生在程序结束),它将输出一个名为.-end.heap的文件(是自动诊断的,是argv[0]的内容)。heap-checker将会对比这两个文件。如果第二个文件使用了过多的内存使用量,NoLeaks()函数将会返回false。对于全程序,这种情况将会引起程序中断。所有情况下都将会打印如何处理dump文件的信息。

项目用到了tcmalloc,然后发现项目有内存泄露,因此略微玩了一下Google Perftools

1.安装

目前Google Perftools的Repo在https://github.com/gperftools/gperftools/tree/master

我们可以下载8.0的Release版本 https://github.com/gperftools/gperftools/tree/at8_0-release

下载之后进行解压:

> cd google-perftool
> ./autogen.sh
> ./configure --prefix=<build_path>
> make
> make install

安装成功!

2.使用

使用方法很简单,目前我们项目使用方式如下(Linux下):

export LD_PRELOAD=<build_path>/lib/libtcmalloc.so

3.Heap Profile

我们可以将tcmalloc使用Heap的信息dump出来,通过pprof工具进行查看。

env HEAPPROFILE="./perf_log/game.log" HEAP_PROFILE_ALLOCATION_INTERVAL=10000000 <program>

HEAPPROFILE: 用于指明dump出来的log信息的前缀
HEAP_PROFILE_ALLOCATION_INTERVAL: 定义采样频率,默认是1G,当tcmalloc分配内存到达配置值时,将会dump出log

查看信息

比如我们的是python

pporf --text /usr/bin/python game.log.0001.heap

4.Heap Leak Profile

我们可以使用tcmalloc的Heap Checker去检查内存泄露

env HEAPCHECK=normal <program>

Heap Checker会在程序结束的时候打印出leak数据,并会提示使用什么指令进行详细信息的查看。

–text: 文本数据
–pdf: 调用栈的信息

5.References

  1. http://dirtysalt.info/gperftools.html#orgheadline2 – 对log信息的说明

好久没有写文章了,一是因为最近公司项目比较紧,二是在看c++,工作两年多c++的知识都忘记了,今天就来简单总结一下c++11的右值引用。
提前声明,本文参考了从4行代码看右值引用

1.右值引用要解决的问题

c++11引入右值引用要解决C++98/03中遇到的两个问题,第一个问题就是临时对象非必要的昂贵的拷贝操作,第二个问题是在模板函数中如何按照参数的实际类型进行转发。
在c++11中所有的值分为三种类型:左值、将亡值、纯右值。纯右值是非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等;将亡值是将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。

2.几个尝试性的测试

测试基本代码如下:

#include <iostream>
using namespace std;

int g_contr_cnt = 0;
int g_decontr_cnt = 0;
int g_copy_cnt = 0;

class A {
public:
    A() {
        cout << "construct: " << ++g_contr_cnt << endl;
    }

    A(const A& oth) {
        cout << "copy: " << ++g_copy_cnt << endl;
    }

    ~A() {
        cout << "deconstruct: " << ++g_decontr_cnt << endl;
    }
};

A getVal() {
    return A();
}
测试1:值拷贝
A x = getVal();

由于编辑器会对代码进行优化,因此我们在编译上面的代码的时候,加入-fno-elide-constructors,运行结果如下:

construct: 1
copy: 1
deconstruct: 1
copy: 2
deconstruct: 2
deconstruct: 3

调用了两次复制构造函数,一次在返回值,一次在对x的赋值。如果A类的复制构造函数不是简简单单地打印信息,而是有大量内存的拷贝,那怎么办?两次复制构造函数性能开销就会很大,并且关键的是拷贝源实例是一个将亡值,留下它根本没有什么用。

换一种方案:const A&

const A& x = getVal();

这时候输出结果如下:

construct: 1
copy: 1
deconstruct: 1
deconstruct: 2

但const reference是不能修改引用的内容的!

另一个方案:右值引用

A&& x = getVal();

这样输出结果与上面一种方案是一样的,并且x并不是一个const类型。右值引用将返回的值“续命”了。

测试2:移动构造函数

我们现在类的定义中加入移动构造函数:

A(A&& oth) {
    cout << "move: " << ++g_move_cnt << endl;
}

这样子,我们再看一下输出结果:

A x = getVal();

输出结果如下:

construct: 1
move: 1
deconstruct: 1
move: 2
deconstruct: 2
deconstruct: 3

没有拷贝构造函数了,与之代替的是移动构造函数,移动构造函数的实现,我们可以认为移动源实例是一个将亡值,这样我们在移动构造函数的内部实现中只需要将移动源实例move到新的实例中即可。

测试3:参数A&&

void setVal(A&&);
void setVal(const A&);

这两个函数,编译器是怎样判断的呢?做了一些尝试:

A x;
setVal(x);   // 左值
setVal(std::move(x));   // 右值
setVal(A());   // 右值
A&& y = getVal();
setVal(y);  // 左值

测试4:std::move

std::move是c++11提供移动实例的方式,测试代码如下:

A x;
A y = std::move(x);

y的初始化使用了移动构造函数:

construct: 1
move: 1
deconstruct: 1
deconstruct: 2

如果类A没有移动构造函数,将会使用复制构造函数。

Linux上如下指令可以查看CPU的信息

> lscpu

在我们线上机器上的结果如下:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                32
On-line CPU(s) list:   0-31
Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 62
Model name:            Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz
Stepping:              4
CPU MHz:               1320.468
CPU max MHz:           2500.0000
CPU min MHz:           1200.0000
BogoMIPS:              4000.99
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              20480K
NUMA node0 CPU(s):     0-7,16-23
NUMA node1 CPU(s):     8-15,24-31

上面的信息,有几个比较有意思:

Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             2
什么是Socket

Socket就是主板上插CPU的槽的数量

什么是Core

Core就是平时说的核,双核、四核等,就是每个CPU上的核数

什么是Thread

thread就是每个core上的硬件线程数,即超线程

对操作系统来说,其逻辑CPU的数量就是Socket*Core*Thread

参考资料

[1] Getting CPU architecture information with lscpu
[2] 处理器三个概念理解及延伸(socket,core,thread,SMT,CMP,SMP)

很久以前就想写一下关于虚拟内存管理方面的文章,主要还是写给自己看的。原因主要是自己对Linux虚拟内存管理也是一知半解,写一篇博客,记录一下自己这几天的学习笔记,留着以后做参考。

这一篇主要讲一下虚拟地址到物理地址的转换。主要参考《深入理解计算机系统》一书。

关于逻辑地址、线性地址和物理地址

物理地址:

物理地址可以想象成实际物理存储上的地址

线性地址:

线性地址也可以叫做虚拟地址,是一个不真实的地址,可以理解为页式管理中的地址

逻辑地址:

逻辑地址与线性地址一样,是一个不真实的地址,用于Intel段式管理中的地址

三个地址的转换关系:

逻辑地址–>线性地址–>物理地址

逻辑地址–>线性地址

逻辑地址由两部分组成,段标识符和段内偏移地址。段标识符是有个16位长的字段组成,叫做段选择符。其中高13位是一个索引号,如下图:

索引号用于索引段描述符表的某一段描述符,段描述符格式如下图:

在Linux下,段描述符的Base都是0,因此逻辑地址的段内偏移量就是线性地址,接下来看一下线性地址怎样转换成物理地址的。

线性地址–>物理地址

首先宏观上的看一下线性地址(虚拟地址)到物理地址的转换

虚拟地址高位有一个页表索引,通过页表索引可以在页表中找到对应的一个页表入口(Page Table Entry),在PTE中可以得到物理页号,然后在加上原先在虚拟地址中的偏移量,就会定位到最终的地址。

基本流程

如上图是一个正常的命中流程:

  1. 处理器生成一个虚拟地址,并将其给MMU
  2. MMU生成一个PTE地址,从高速缓存/主存中请求获取
  3. 高速缓存/主存将PTE数据传回MMU
  4. MMU生成物理地址,传给高速缓存/主存
  5. 高速缓存/主存将数据传回处理器

上图是发生缺页中断的情况,高速缓存/主存会将牺牲页换出,将新页换进。

利用TLB加速地址翻译

TLB全称翻译后备缓冲器(Translation Lookaside Buffer),位于MMU内存,其实就是缓存住了一些PTE,那么MMU就不需要从高速缓存/主存中获取PTE了。

多级页表

如果是32位的虚拟地址空间,一个页的大小为4K,那一张页表就有1M个PTE,如果每个PTE的大小为4个字节,那么页表占用内存大小为4MB。如果是64位机器,它的寻址位数是48位,那页表就更大了。
解决方式是讲页表分为多级,如下图:

地址翻译的过程基本就差不多了。

本篇文章是我几个月前在CSDN的blog上写的,现在移过来

折腾了两天,终于将Mangos Zero在自己的Windows电脑上搭建起来了,下面总结一下安装Mangos Zero过程。网上也有很多安装Mangos Zero的教程,都比较老了,有些基本不能用了。

1. 安装环境

  1. Windows XP 64位

2. 前期准备

  1. MySQL(本人使用的是5.5的版本)
  2. SQLyog(这个根据个人喜好,就是MySQL的GUI)
  3. Git
  4. World of Warcraft 1.12 (Google找一下,我这边有一个,但忘记网址了,过一阵子上传到网盘上)
  5. VS 2010 或更高版本 (本人是VS2013)

3. 安装MySQL

MySQL的安装我就不多说了,不会的网上一大堆教程的,这里要记住MySQL安装过程中设置的密码,后面会用到。

4. 安装Git

安装指引一步一步安装就好,没有什么可注意的地方。

5. 创建一个根目录

创建一个名为Mangos的根目录,下面的所有操作我们都会在这个目录下进行

6. 下载database

> cd Mangos
> git clone --recursive http://github.com/mangoszero/database.git -b develop21

这里要多少几句,我使用的是develop21分支,一开始我使用的是release20这个分支,但发现database和server的版本不匹配,并且按网上的说法打Patch都解决不了,所有就用了最新的开发分支,经本人测试完全可用。因为是开发分支,可能开发者在上面修改导致一些其他问题,所以我自己也fork了一个这样的分支,大家也可用clone我自己的git

git clone --recursive https://github.com/Whosemario/database.git -b develop21

7.将数据加载到MySQL

> cd Mangos
> cd database
> InstallDatabases.bat

起始就是运行database文件夹下的InstallDatabases.bat脚本。跟着提示一路配置下去。几点要注意:

  1. 第一步输入N进行下一步
  2. MySQL的密码就是上面安装MySQL设置的密码
  3. 没有其他什么要求,剩下的配置都使用默认就好,下面的教程将根据默认配置来
    如果一切顺利,你会在MySQL里面看到三个新的数据库,如下
    这里写图片描述
    它们分别是character0 mangos0 realmd
    此时还没完,这里的character0的版本会和mangos server的版本不一致,打开character0的db_version这张表。
    这里写图片描述
    它的structure一列位1,但server需要的版本为2,所以我们要为character0打个Patch,在database/Character/Updates/rel21/目录下有一个sql文件Rel21_2_0_honor_flush.sql,将其运行即可。

8. 下载server

> cd Mangos
> git clone https://github.com/mangoszero/server.git --recursive -b develop21

相同的,我也有一个对应的repo

git clone https://github.com/Whosemario/server.git -b develop21

9. 编译server

到Mangos/server/win目录下,里面有很多安装软件,分别安装cmake和openssl,然后运行MaNGOS_EasyBuild.exe,点击按钮Check Now,如果上面的安装一切正常,会如图
这里写图片描述
点击Build Options,会有一些基本选项,按自己的需求去选择,没有什么大问题,然后点击Generate Project,最后会打开你的Visual Studio,然后rebuild整个项目,项目rebuild成功后,会在Mangos文件加下生成server_build的目录,然后进行下面两步操作:

  1. 将server_build/bin/Debug/下的文件都拷贝到你的WoW客户端的根目录(也就是WoW.exe所在的目录)
  2. 再将server_build/bin/Debug/tools/下的文件都拷贝到你的WoW客户端的根目录
  3. 在WoW客户端目录下,运行ExtractResources.sh,跟着指引一步一步配置,当运行完后,会生成三个目录 dbc maps vmaps
  4. 在Mangos目录下新建名为Data的文件夹,将上面的三个目录拷贝到Data文件夹下

10. 配置Mangos Server

  1. 进入Mangos/server_build/bin/Debug/文件夹下,将mangosd.conf.dist文件重命名为mangosd.conf,并修改里面的内容

    DataDir = "."    
    

    修改为

    DataDir = "C:\Mangos\Data"
    

    假设你的Mangos新建在C盘根目录下。

    LoginDatabaseInfo      "127.0.0.1;3306;mangos;mangos;realmd"
    WorldDatabaseInfo      = "127.0.0.1;3306;mangos;mangos;mangos"
    ScriptDev2DatabaseInfo = "127.0.0.1;3306;mangos;mangos;mangos"
    CharacterDatabaseInfo  = "127.0.0.1;3306;mangos;mangos;character0"
    

修改为:

LoginDatabaseInfo      = "127.0.0.1;3306;root;123456;realmd"
WorldDatabaseInfo      = "127.0.0.1;3306;root;123456;mangos0"
ScriptDev2DatabaseInfo = "127.0.0.1;3306;root;123456;mangos0"
CharacterDatabaseInfo  = "127.0.0.1;3306;root;123456;characters"

假设你的MySQL密码为123456.

  1. 将realmd.conf.dist文件重命名为realmd.conf

    LoginDatabaseInfo     = "127.0.0.1;3306;mangos;mangos;realmd"
    

    改为:

    LoginDatabaseInfo     = "127.0.0.1;3306;root;123456;realmd"
    
  2. 修改realmd数据库里面的realmlist表,将第一列带有MaNGOS的字符串替换为MyWoW

11. 修改客户端

  1. 打开realmlist.wtf,将最后的域名改为127.0.0.1

12. 启动游戏

  1. 打开Mango\server_build\bin\Debug\realmd.exe
    这里写图片描述
  2. 打开Mango\server_build\bin\Debug\mangosd.exe
    这里写图片描述
  3. 新建一个玩家
    这里写图片描述
  4. 打开客户端WoW.exe
    这里写图片描述

    reference

  5. How to Setup a MaNGOS Rel21 Server from Scratch