protobuf

今天有同事问我,使用protobuf 作为通信协议之后,是否还需要压缩呢。
其实我们知道,protobuf 为了节省字节数,本身已经做了一些处理,例如 采用 Varint 编码,ZigZag编码,Length-delimited编码等,并且对repeated的字段提供了packed的处理,其实已经算做了优化了,但是,如果你的数据量很大的话,其实是可以在继续使用压缩算法去压缩的,因为压缩的话,要在cpu消耗和流量之间做一个权衡,一般来说,小消息就没必要了,当消息达到一定程度(例如100byte以上),压缩还是有效果的, 一般压缩的话,zlib压缩效率是最高的,但是比较消耗cpu, 一般我是采用snappy,虽然压缩效率不高,但是它在cpu消耗和压缩效率之前的权衡做的比较合理。

这几篇关于protobuf原理的文章将的挺好,有兴趣的可以读读。
https://developers.google.com/protocol-buffers/docs/encoding#structure
https://blog.csdn.net/zxhoo/article/details/53228303
http://www.okyes.me/2016/09/10/protobuf-encode.html

Reed-Solomon Erasure Code

EC的定义:erasure code是一种技术,它可以将n份原始数据,增加m份数据(n > m),并能通过n+m份中的任意n份数据,还原为原始数据。定义中包含了encode和decode两个过程,将原始
的n份数据变为n+m份是encode,之后这n+m份数据可存放在不同的device上,如果有任意小于m份的数据失效,仍然能通过剩下的数据还原出来。

EC主要是通过纠删码算法将原始的数据进行编码得到冗余,并将数据和冗余一并存储起来,以达到容错的目的。

在数据存储领域, 一般有多副本,EC两种数据冗余技术,EC的话,可以很大程度上增加我们磁盘的利用率。
例如:

1
2
3
4
5
多副本
1个副本,假设我们存储2个备份副本,那么总共就3个副本了,10个副本数据就是30个副本要存储,
这样我们同一个数据也只是能容忍丢掉3次。
EC
1个副本,10个副本数据我们如果n=10, m=4, 只需要14个副本。这样我们可以容忍你丢掉任意3个副本。

在直播,游戏领域,采用EC可以增加我们的抗丢包能力。

EC原理不复杂,理解的话需要一些矩阵的知识,例如

  1. 逆矩阵。
  2. 单位矩阵。
  3. 矩阵*逆矩阵= 单位矩阵。
  4. 任何一个矩阵乘以单位矩阵都不变。

详细原理和代码可以参考以下资料。

Backblaze blog.
Forward error correction
Reed-Solomon Erasure Coding in Go

kcp_erlang

这是一个erlang版本的kcp实现,这里的定位,暂时只是作为demo,供大家学习与参考,如果要变成一个成熟的库还需要加不少东西。后面如果时间稍微充裕一些的话,会继续完善。

Demo

kcp-Socket

整理了一份c#版本的Kcp, Demo
可以直接在Unity里面使用,我在代码里添加了一些注释,让小伙伴们可以更快的理解Kcp和Demo。

Demo里面有些功能缺失了,例如发送的流模式和消息模式,现在只有消息模式,后面博主有时间的话会改过来。

博主昨天调试了Demo和一份Kcp-Erlang版本的进行通信,已经完成了,但两边细节都还需要优化,后续做完会放出来。

nifty

本文章主要讲 nifty 的安装过程,因为这次坑似乎略多,所以在这里记录下。
github 地址 : https://github.com/parapluu/nifty.git

环境:
CentOS release 6.8 (Final)

项目的README.md文件中可以看到,nifty的依赖主要是clang compiler 和 libclang
先看http://clang.llvm.org/get_started.html官方给出的安装说明。
大概需要的文件这里列下

1
2
3
4
5
Package		Version			Notes
GNU Make 3.79, 3.79.1 Makefile/build processor
GCC >=4.8.0 C/C++ compiler1
python >=2.7 Automated test suite2
zlib >=1.2.3.4 Compression library3

大概分4大块来安装

1. gcc ,python 版本 。
devtoolset-3-toolchain ansible demo
python ansible demo

安装devtoolset-3-toolchain来解决GCC版本问题,安装完成之后开启就可以。
关于如何安装devtoolset-3-toolchain 可以看我的ansible demo 中关于gcc的升级和开启。
剩下的python zlib这些比较简单自己装了。
python部分也可以看我的ansible demo中关于python安装的部分。

2. cmake

wget https://cmake.org/files/v3.7/cmake-3.7.1.tar.gz
tar xzf cmake-3.7.1.tar.gz
cd cmake-3.7.1
./bootstrap
gmake -j$(nproc)
make install
cd ..

3. clang(这里安装的时候在make -j$(nproc) 的时候博主层出现过报错,后面的解决方式是加内存,详细的报错原因没留了,这个大家装的时候可以留意下。可能有人留意到了这里不是官方给出的安装方式,其实是一样的,只是官方的example是trunk的例子,这里是用了release版本。

wget http://llvm.org/releases/3.9.1/llvm-3.9.1.src.tar.xz
wget http://llvm.org/releases/3.9.1/cfe-3.9.1.src.tar.xz
wget http://llvm.org/releases/3.9.1/compiler-rt-3.9.1.src.tar.xz
wget http://llvm.org/releases/3.9.1/clang-tools-extra-3.9.1.src.tar.xz
tar xf llvm-3.9.1.src.tar.xz
mv llvm-3.9.1.src llvm
cd llvm/tools
tar xf ../../cfe-3.9.1.src.tar.xz
mv cfe-3.9.1.src clang
cd clang/tools
tar xf ../../../../clang-tools-extra-3.9.1.src.tar.xz
mv clang-tools-extra-3.9.1.src extra
cd ../../../projects
tar xf ../../compiler-rt-3.9.1.src.tar.xz
mv compiler-rt-3.9.1.src compiler-rt
cd ../..
mkdir llvm-build
cd llvm-build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/home/data/software/clang3.9.1 -DLLVM_OPTIMIZED_TABLEGEN=1 ../llvm
make -j$(nproc)
make install

4. libclang.so
链接

前面都安装成功之后,在nitfy:compile的时候还会报错,那大概是报libclang.so not found, 这个问题只需要设置下环境变量 LD_LIBRARY_PATH 为libclang.so的路径就可以。
详细的解决方案看连接。

至此,整个nifty已经安装完成了, 注意前面的操作都需要在 gcc>4.8 上操作,也就是说你的devtoolset-3要一直启动着,不然会报很多奇怪的问题。

UGUI Resoulution

UGUI针对Android五花八门分辨率的屏幕自适应。

  1. Canvas 选择Screen Space-Camera 模式。
  2. Canvas 设置成Orthographic模式。
  3. Canvas Scaler 选择 Scale With Screen Size,并且 Screen Match Mode 选择 Match Width Or Height, 比例设置为0,只和宽度进行适配。

代码上的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InitResolution() {
if (ClientConfig.PLATFORM != PlatformType.Android) {
return;
}

int width = Screen.currentResolution.width;
int height = Screen.currentResolution.height;
int targetWidth = 960;
int targetHeight = 640;

if (width <= targetWidth || height <= targetHeight)
return;

float targetRatio = targetWidth / (float)targetHeight;
float currentRatio = width / (float)height;

if (targetRatio < currentRatio) {
targetWidth = (int)Mathf.CeilToInt(targetHeight * currentRatio);
} else if (targetRatio > currentRatio) {
targetHeight = (int)Mathf.FloorToInt(targetWidth / currentRatio);
}
Screen.SetResolution(targetWidth, targetHeight, true);
}

基本是这样,现在似乎有一款手机(哪一款忘记了)屏幕宽度是高度的2倍+的,这个要特殊处理下,
直接把Canvas Scaler 的 Match Width Or Height 比例设置成1。

速读代码

今天看到一些人在网上讨论关于读代码的问题,引发了一些思考。这里我写下个人的一些看法。

可能大家遇到过一样的问题,发现github某款项目的核心思想很好,我们想去了解它具体的实现,但是直接去撸的话发现代码量庞大,甚至有一些奇淫技巧在里面,读起来吃力且费时间,并且因为我们的目的很明确,只想了解它的核心思想实现,不想花费太多的时间在其他方面。

对于这种情况,可以选择的一个方法是翻他的历史记录,去读他的第一版或前几个版本的的代码,通常早期的版本可以用一个词来总结:”麻雀虽小五脏俱全”, 这个版本的代码量通常不多,并且设计的思想很明确,思路很清晰,处理问题具有针对性,这个阶段的源码比较适合我们去了解它的核心思想或者说背后的设计理念,而且一个流程下来我们花费的时间也不多。

至于后面代码变的复杂,因为核心功能确定之后,代码不断的重构和优化,版本不断的迭代,堆叠起来就渐渐的把项目弄复杂。

这或许就是古语中说的:”拨开云雾见月明”。

GAME-AI

AI是artificial intelligence的简称,人工智能,在游戏项目开发中,AI也是很重要的一部分。

现在流行的AI写法,主要有3种: FSM(状态机), HFSM(分层状态机)和BT(行为树)。
网上资料很多,但对于初学者来说可能会有点迷,因为无法从众多的资料中挑选出价值相对较高的文章,这里博主就列出一些比较有价值的文章和代码,节省初学者的时间(这里只是给需要的了解这方面的同学一点点小方向)。

  1. 腾讯开源的Behavior
  2. AI分享站
  3. 游戏AI—行为树研究及实现

腾讯的Behavior很有研究价值。
这3样东西下肚,知识是可以的,剩下就只需要在实践中把知识转变成能力。

(博主本来想写篇c#版本的behavior tree来讲讲的,但是觉得应该没这些资料写得详细,所以还是给大家指指方向,少走弯路。同时也记录下自己以前学习的脚印,共勉)

Abstract Factory

在面向对象的程序语言中,我们经常听到设计模式这个名词,其实呢,大部分设计模式我们在写项目的过程中都或多或少的用到了, 例如 Template, Strategy, Adapter, Proxy, Builder等等,只是可能我们不知道它被称呼为这个名字。
虽然设计模式的类型繁多, 但是有些是比较经典的(可能让人第一次见就觉得醒醐灌顶吧。),
例如Singleton, Abstract Factory, Observer, Visitor等。

Singleton和ObServer在项目中应该都是遍地的,Visitor其实本质是函数式语言中的”模式匹配”,
这里就写篇关于Abstract Factory在游戏中的一个应用。

在游戏中,我们经常需要创建各类怪物,角色,宠物,陷阱等等。
如若需要根据副本类型优雅的创建各类怪物,并且尽量的降低耦合度,用Abstract Factory 模式是一个很好的选择, 下面贴下我写的代码。(也是Abstract Factory在游戏中的一种使用方式, 代码里面在创建相应的类型对象的时候使用了SimpleFactory模式.)

最后,说下个人观点
关于设计模式在程序中应用,也要把握适当才行,如果你觉得你的代码是直接的,精巧的,简单的,那么就是最好的,如果在写代码的时候过分在意设计模式,可能会让本身应该简洁的代码绕了几个弯哦~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public enum OBJ_TYPE{
MON = 1,
PET = 2,
PLAYER = 3
}

public enum MON_TYPE{
NORMAL = 1,
LEGEND = 2
}

public enum PET_TYPE{
NORMAL = 1,
LEGEND = 2
}

public enum PLAYER_TYPE{
NORMAL = 1,
LEGEND = 2
}

abstract class Ab_ObjFactory{
public abstract List<Ab_Obj> Create();
}

abstract class Ab_Obj{

public abstract void Init();

protected OBJ_TYPE _type = 0;
public abstract OBJ_TYPE Type {
get;
set;
}

public static T Create<T>() where T : Ab_Obj, new(){
T t = new T ();
return t;
}

}

abstract class Ab_ObjMon : Ab_Obj{
public override OBJ_TYPE Type{
get{
return OBJ_TYPE.MON;
}
set{
_type = OBJ_TYPE.MON;
}
}

protected MON_TYPE _subType;
public abstract MON_TYPE SubType{
get;
set;
}
}

abstract class Ab_ObjPet : Ab_Obj{
public override OBJ_TYPE Type{
get{
return OBJ_TYPE.PET;
}
set{
_type = OBJ_TYPE.PET;
}
}

protected PET_TYPE _subType;
public abstract PET_TYPE SubType{
get;
set;
}


}

abstract class Ab_ObjPlayer : Ab_Obj{
public override OBJ_TYPE Type{
get{
return OBJ_TYPE.PLAYER;
}
set{
_type = OBJ_TYPE.PLAYER;
}
}

protected PLAYER_TYPE _subType;
public abstract PLAYER_TYPE SubType{
get;
set;
}
}


class ObjNormalMon : Ab_ObjMon{
public override MON_TYPE SubType{
get{
return MON_TYPE.NORMAL;
}
set{
_subType = MON_TYPE.NORMAL;
}
}
public override void Init(){
Debug.Log("创建对象 NormalMon");
}

}

class ObjLegendMon : Ab_ObjMon{
public override MON_TYPE SubType{
get{
return MON_TYPE.LEGEND;
}
set{
_subType = MON_TYPE.LEGEND;
}
}
public override void Init(){
Debug.Log ("创建对象 LegendMon");
}
}

class ObjNormalPet : Ab_ObjPet{
public override PET_TYPE SubType{
get{
return PET_TYPE.NORMAL;
}
set{
_subType = PET_TYPE.NORMAL;
}
}
public override void Init(){
Debug.Log("创建对象 NormalPet");
}
}

class ObjLegendPet : Ab_ObjPet{
public override PET_TYPE SubType{
get{
return PET_TYPE.LEGEND;
}
set{
_subType = PET_TYPE.LEGEND;
}
}

public override void Init(){
Debug.Log("创建对象 LegendPet");
}
}

class ObjNormalPlayer : Ab_ObjPlayer{
public override PLAYER_TYPE SubType{
get{
return PLAYER_TYPE.NORMAL;
}
set{
_subType = PLAYER_TYPE.NORMAL;
}
}

public override void Init(){
Debug.Log("创建对象 NormalPlayer");
}
}

class ObjLegendPlayer : Ab_ObjPlayer{
public override PLAYER_TYPE SubType{
get{
return PLAYER_TYPE.LEGEND;
}
set{
_subType = PLAYER_TYPE.LEGEND;
}
}

public override void Init(){
Debug.Log ("创建对象 LegendPlayer");
}
}


class DungeonAFactory : Ab_ObjFactory{
public override List<Ab_Obj> Create(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
objList.AddRange( CreateMon ());
objList.AddRange( CreatePet ());
return objList;
}

public List<Ab_Obj> CreateMon(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
ObjNormalMon normalMon = Ab_Obj.Create<ObjNormalMon> ();
normalMon.Init ();
objList.Add (normalMon);
ObjLegendMon legendMon = Ab_Obj.Create<ObjLegendMon> ();
legendMon.Init ();
objList.Add (legendMon);
return objList;
}

public List<Ab_Obj> CreatePet(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
ObjNormalPet normalPet = Ab_Obj.Create<ObjNormalPet>();
normalPet.Init ();
objList.Add (normalPet);
ObjLegendPet legendPet = Ab_Obj.Create<ObjLegendPet> ();
legendPet.Init ();
objList.Add (legendPet);
return objList;
}
}

class DungeonBFactory : Ab_ObjFactory{
public override List<Ab_Obj> Create(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
objList.AddRange( CreateMon ());
objList.AddRange( CreatePet ());
return objList;
}

public List<Ab_Obj> CreateMon(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
ObjLegendMon legendAMon = Ab_Obj.Create<ObjLegendMon> ();
legendAMon.Init ();
objList.Add (legendAMon);
ObjLegendMon legendBMon = Ab_Obj.Create<ObjLegendMon> ();
legendBMon.Init ();
objList.Add (legendBMon);
return objList;
}

public List<Ab_Obj> CreatePet(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
ObjLegendPet legendAPet = Ab_Obj.Create<ObjLegendPet> ();
legendAPet.Init ();
objList.Add (legendAPet);
ObjLegendPet legendBPet = Ab_Obj.Create<ObjLegendPet> ();
legendBPet.Init ();
objList.Add (legendBPet);
return objList;
}
}

class ArenaFactory : Ab_ObjFactory{

public override List<Ab_Obj> Create(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
objList.AddRange( CreatePlayer ());
return objList;
}

public List<Ab_Obj> CreatePlayer(){
List<Ab_Obj> objList = new List<Ab_Obj> ();
ObjNormalPlayer normalAPet = Ab_Obj.Create<ObjNormalPlayer> ();
normalAPet.Init ();
objList.Add (normalAPet);
ObjLegendPlayer legendBPet = Ab_Obj.Create<ObjLegendPlayer> ();
legendBPet.Init ();
objList.Add (legendBPet);
return objList;
}
}

public class main : MonoBehaviour {

void Awake() {
DungeonAFactory normalMonFactory = new DungeonAFactory();
DungeonBFactory normalPetFactory = new DungeonBFactory();
ArenaFactory arenaFactory = new ArenaFactory ();
normalMonFactory.Create ();
normalPetFactory.Create ();
arenaFactory.Create ();
}

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}




}

用c#写的,在Unity里面挂上脚本贴入代码可以看到效果。

Erlang Cookie

最近在写一个cluster_demo, 遇到一个问题,我的集群中的两个节点在windows下大概是这样启动的。

erl -name a@127.0.0.1 -setcookie aa
erl -name b@127.0.0.1 -setcookie bb

因为我知道,节点间的互连要cookie一致,我们可以通过下面这个函数设置cookie

erlang:set_cookie(Node, NodeCookie)

或许大家看过官方文档和一些博客里面写的,集群节点可以不需要全部节点同一个cookie,只需要使用erlang:set_cookie去设置你想要连接节点的cookie。
我把这句话理解点代码是这样:

a@127.0.0.1节点上,我进行如下操作

erlang:set_cookie('b@127.0.0.1', bb)

b@127.0.0.1节点上,我进行如下操作

erlang:set_cookie('a@127.0.0.1', aa)

我以为这样的话,那么我们就可以在a或者b上直接ping通对方,但是事实却不是这样,当我在 a@127.0.0.1 节点上执行

net_adm:ping('b@127.0.0.1')

他的连接并没有成功,并且提示在b@127.0.0.1上给我的提示是

** Connection attempt from disallowed node ‘a@127.0.0.1' **

我搜索了erlang的kernel的库代码,找到了2个地方会存在这个提示,

erlang kernel version: kernel-4.1.1

dist_util.erl 151行

1
2
3
4
5
6
7
8
9
10
11
%% check if connecting node is allowed to connect with allow-node-scheme
is_allowed(#hs_data{other_node = Node,
allowed = Allowed} = HSData) ->
case lists:member(Node, Allowed) of
false when Allowed =/= [] ->
send_status(HSData, not_allowed),
error_msg("** Connection attempt from "
"disallowed node ~w ** ~n", [Node]),
?shutdown(Node);
_ -> true
end.

dist_util.erl 591行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%% wait for challenge response after send_challenge
recv_challenge_reply(#hs_data{socket = Socket,
other_node = NodeB,
f_recv = FRecv},
ChallengeA, Cookie) ->
case FRecv(Socket, 0, infinity) of
{ok,[$r,CB3,CB2,CB1,CB0 | SumB]} when length(SumB) =:= 16 ->
SumA = gen_digest(ChallengeA, Cookie),
ChallengeB = ?u32(CB3,CB2,CB1,CB0),
?trace("recv_reply: challenge=~w digest=~p\n",
[ChallengeB,SumB]),
?trace("sum = ~p\n", [SumA]),
case list_to_binary(SumB) of
SumA ->
ChallengeB;
_ ->
error_msg("** Connection attempt from "
"disallowed node ~w ** ~n", [NodeB]),
?shutdown(NodeB)
end;
_ ->
?shutdown(no_node)
end.

很明显,不是151行的问题,通过设置 net_kernel:allow([]) 可以排除这个问题。
看代码可以发现 当SumB =/= SumA的时候就会返回这个提示,SumB是接收到的,SumA是我们计算出来的,

SumA = gen_digest(ChallengeA, Cookie)

SumA是这样生成的,查看了ChallengeA的生成代码,只是进行一些算数,然后生成的一个值

1
2
3
4
5
6
7
8
9
10
11
12
13
gen_challenge() ->
A = erlang:phash2([erlang:node()]),
B = erlang:monotonic_time(),
C = erlang:unique_integer(),
{D,_} = erlang:statistics(reductions),
{E,_} = erlang:statistics(runtime),
{F,_} = erlang:statistics(wall_clock),
{G,H,_} = erlang:statistics(garbage_collection),
%% A(8) B(16) C(16)
%% D(16),E(8), F(16) G(8) H(16)
( ((A bsl 24) + (E bsl 16) + (G bsl 8) + F) bxor
(B + (C bsl 16)) bxor
(D + (H bsl 16)) ) band 16#ffffffff.

打印Cookie的值,发现是aa ,正式我们在b@127.0.0.1上设置的a@127.0.0.1的cookie。
基本上能断定出问题的就是这个cookie值了。

但这里有一个情况出现,如果我们两个脚本的启动时这样的

erl -name  a@127.0.0.1 -setcookie aa
erl -name  b@127.0.0.1 -setcookie aa

我们可以直接net_adm:ping()成功对方。
我们查看官方文档,erlang:set_cookie()的解释

Sets the magic cookie of Node to the atom Cookie. If Node is the local node, the function also sets the cookie of all other unknown nodes to Cookie (see Section Distributed Erlang in the Erlang Reference Manual in System Documentation).

因为我们启动的时候setcookie ,按照官方文档的解释,其实在a@127.0.0.1节点上也可以说是进行了这样操作的

erlang:set_cookie('b@127.0.0.1', aa)

同理,在b@127.0.0.1上就是

erlang:set_cookie('a@127.0.0.1', aa)

这样我们可以发现,其实他们可以互连,是因为他们都设置对方的cookie为同一个值,我们做个尝试,在两个节点上分别执行下面2个操作

erl -name a@127.0.0.1 -setcookie aa
erlang:set_cookie('b@127.0.0.1', cc)

erl -name b@127.0.0.1 -setcookie bb
erlang:set_cookie('a@127.0.0.1', cc)

然后在进行net_adm:ping()发现,他们pong了。
我们可以得出结果,其实他们要互连,是需要双方将对方的cookie设置为同一个值。

我们在去翻下 Distributed Erlang章节的文档,发现其实是有这句话的,

For a node Node1 with magic cookie Cookie to be able to connect to, or accept a connection from, another node Node2 with a different cookie DiffCookie, the function erlang:set_cookie(Node2, DiffCookie) must first be called at Node1. Distributed systems with multiple user IDs can be handled in this way.

仔细看,他只是说需要在Node1 上 erlang:set_cookie(Node2, DiffCookie) ,并没有说 Node2上也需要调用erlang:set_cookie()啊。。。。。。额,其实我一直以为是文档不详细。。。。。。