<?xml version="1.0" encoding="gb2312"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>云风的 BLOG</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/" />
    <link rel="self" type="application/atom+xml" href="http://blog.codingnow.com/atom.xml" />
   <id>tag:blog.codingnow.com,2012://1</id>
    <link rel="service.post" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1" title="云风的 BLOG" />
    <updated>2012-05-15T09:18:58Z</updated>
    <subtitle>思绪来得快去得也快，偶尔会在这里停留</subtitle>
    <generator uri="http://www.sixapart.com/movabletype/">Movable Type 3.2b5</generator>
 
<entry>
    <title>开发笔记(19) : 怪物行走控制</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/05/dev_note_19.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=762" title="开发笔记(19) : 怪物行走控制" />
    <id>tag:blog.codingnow.com,2012://1.762</id>
    
    <published>2012-05-15T09:17:48Z</published>
    <updated>2012-05-15T09:18:58Z</updated>
    
    <summary>这段时间项目进展还算顺利，叮当同学在盯项目进度，我专心解决程序上的各种小问题。 最近我在协助解决 NPC （包括地图上的怪物）的行为控制以及 AI 的问题。 目前，我们的进度还处在玩家可以通过客户端登陆到服务器，可以在场景上漫游，以及做一些简单的战斗和技能动作的阶段。按最初的设计原则，我们的每个玩家是在服务器上有一个独立的 agent 服务的。目前写到现在，和中间想过的一些实现方法有些差异，但大体上还是按这个思路进行的。 关键是在于去除大部分的回调方式的异步调用；编写的控制流程自然完整，不需要太多的去考虑 agent 行为之外的交互性。 如果丝毫不考虑性能问题，我很想把每单个怪物和 NPC 都放在独立服务中。但是估算后，觉得不太现实。在 这一篇 的最后一段已经提过这个问题了。 今天展开来谈谈我的方案。 我想把怪物的移动行为独立出来做，以减少 AI 的压力。也就是说，地图上所有的怪，在设定的时候，都可以设定他们的巡逻路径，或是仅仅站立不动。我希望在没有外力干扰的时候，处理这些行为对系统压力最小。 我不想让怪在没有任何玩家看见的时候就让它静止不动，因为这样可能会增加实现的复杂性，并在怪物行为较为复杂时，无法贯彻策划的意图。 最好的方法还是把之隔离，使其对系统的负荷受控。同时也可以通过分离，减小实现的复杂性。 这个子系统是这样的：...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>这段时间项目进展还算顺利，叮当同学在盯项目进度，我专心解决程序上的各种小问题。</p>

<p>最近我在协助解决 NPC （包括地图上的怪物）的行为控制以及 AI 的问题。</p>

<p>目前，我们的进度还处在玩家可以通过客户端登陆到服务器，可以在场景上漫游，以及做一些简单的战斗和技能动作的阶段。按最初的设计原则，我们的每个玩家是在服务器上有一个独立的 <a href="http://blog.codingnow.com/2011/11/dev_note_1.html">agent</a> 服务的。目前写到现在，和中间想过的一些实现方法有些差异，但大体上还是按这个思路进行的。
关键是在于去除大部分的回调方式的异步调用；编写的控制流程自然完整，不需要太多的去考虑 agent 行为之外的交互性。</p>

<p>如果丝毫不考虑性能问题，我很想把每单个怪物和 NPC 都放在独立服务中。但是估算后，觉得不太现实。在 <a href="http://blog.codingnow.com/2012/03/dev_note_14.html">这一篇</a> 的最后一段已经提过这个问题了。</p>

<p>今天展开来谈谈我的方案。</p>

<p>我想把怪物的移动行为独立出来做，以减少 AI 的压力。也就是说，地图上所有的怪，在设定的时候，都可以设定他们的巡逻路径，或是仅仅站立不动。我希望在没有外力干扰的时候，处理这些行为对系统压力最小。</p>

<p>我不想让怪在没有任何玩家看见的时候就让它静止不动，因为这样可能会增加实现的复杂性，并在怪物行为较为复杂时，无法贯彻策划的意图。</p>

<p>最好的方法还是把之隔离，使其对系统的负荷受控。同时也可以通过分离，减小实现的复杂性。</p>

<p>这个子系统是这样的：</p>
]]>
        <![CDATA[<p>它可以接收请求，在单张地图上创建出怪物来。它只关心怪物的坐标。通过 <a href="http://blog.codingnow.com/2011/12/dev_note_6.html">ShareDB</a> 和别的服务分享怪物的坐标。因为它只关心和修改怪物的坐标字段，所以适用于任何结构不同的怪物。</p>

<p>在创建出怪物对象后，可以接受指令，给怪物附加上一个行为。这个行为目前可以是静止，巡逻，跟随别的对象。</p>

<p>这个子系统以一个较低的频率（比如一秒一次），按行为去重新计算所有对象的位置。更新位置后，通过 <a href="http://blog.codingnow.com/2012/02/dev_note_11.html">组播服务</a> 通知地图上所有的玩家 agent 。</p>

<hr />

<p>这个子系统如何和主系统协作呢？</p>

<p>这里所谓主系统就是地图服务器。我们会把怪物的 AI 模块加载进去运行。怪物 AI 模块会由玩家的 agent 去被动驱动。就是说，如果没有 agent 触发 AI 的处理流程，AI 是死的，不占用 CPU 时间的。每个怪物在生成时，同时通知上述子系统构建一个对象，使它可以在地图中游荡。</p>

<p>怪物由子系统驱动游荡时，通过 AOI 子系统，有可能引发相关的处理逻辑。（一般是进入 agent 的视野，进而触发怪物的 AI 处理）</p>

<p>当怪物 AI 模块接管怪物的控制后，通知子系统删除对应对象，然后进入 AI 控制环节。</p>

<hr />

<p>这个子系统可以分处于独立进程，独立 CPU 中，并且可以调节控制频率。所以我们可以把这些游荡怪物对系统的负载影响减轻到最小。让系统负荷仅和地图上玩家数量正相关。</p>

<p>对于复杂的怪物逻辑，比如副本中的 BOSS 还是可以以 agent 等价的形式放在一个独立的服务中去处理，这和上面的系统并无矛盾。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(18) : 读写锁与线程安全</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/05/dev_note_18.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=761" title="开发笔记(18) : 读写锁与线程安全" />
    <id>tag:blog.codingnow.com,2012://1.761</id>
    
    <published>2012-05-04T06:39:01Z</published>
    <updated>2012-05-04T06:41:02Z</updated>
    
    <summary>最近一段时间，我正解决的技术问题都是和多线程以及并发有关。 我期望在产品上线后，可以用的上至少 32 核的机器。这样，让多核平摊计算负荷就比较重要。单机承载的人数可以不高，但不希望玩家卡在一起特定的操作上。 这里强调的是单机上的并发，而把跑在不同机器的进程会分开考虑。所以，尽量利用共享内存以及单机锁来解决信息交换和状态同步问题。 上个月花了很多时间解决在 Lua 中并发读取同一份配置数据 的问题。一开始是想用一个 Lua State 储存一份结构化数据，然后让其它的 State 对他进行并发的读操作。我以为这种并发读的行为是线程安全的，但是我错了。因为读 Lua State 是需要对 Lua Stack 做修改，所以在没有锁的情况并不能做到线程安全。 随之我实现了一个改进方案。一开始就初始化好若干 Lua Thread ，在不同的 OS Thread 中使用独立的 Stack 空间操作。因为我们的框架只会启动有限的 OS 线程来跑更多的 Lua State ，这样做是可行且线程安全的。不过还是有一点小问题。如果用户向 Lua State 压入并不存在的 key (string 类型)...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="技术" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>最近一段时间，我正解决的技术问题都是和多线程以及并发有关。</p>

<p>我期望在产品上线后，可以用的上至少 32 核的机器。这样，让多核平摊计算负荷就比较重要。单机承载的人数可以不高，但不希望玩家卡在一起特定的操作上。</p>

<p>这里强调的是单机上的并发，而把跑在不同机器的进程会分开考虑。所以，尽量利用共享内存以及单机锁来解决信息交换和状态同步问题。</p>

<p>上个月花了很多时间解决<a href="http://blog.codingnow.com/2012/04/lua_multi_states_database.html">在 Lua 中并发读取同一份配置数据</a> 的问题。一开始是想用一个 Lua State 储存一份结构化数据，然后让其它的 State 对他进行并发的读操作。我以为这种并发读的行为是线程安全的，但是我错了。因为读 Lua State 是需要对 Lua Stack 做修改，所以在没有锁的情况并不能做到线程安全。</p>

<p>随之我实现了一个改进方案。一开始就初始化好若干 Lua Thread ，在不同的 OS Thread 中使用独立的 Stack 空间操作。因为我们的框架只会启动有限的 OS 线程来跑更多的 Lua State ，这样做是可行且线程安全的。不过还是有一点小问题。如果用户向 Lua State 压入并不存在的 key (string 类型) ，修改 Lua 中 string pool 的操作又有线程安全问题了。当然，这个问题还可以用更为复杂的方法规避（比如再给每个线程配置一个独立的 state 做 string 合法性校验）。</p>

<p>我最后放弃了直接用 Lua State 储存这份共享数据，而改用自己实现的一个线程读安全的 hash 表。Lua 仅作为数据解析和加载过程的工具而存在。<a href="https://github.com/cloudwu/lua-db">整套代码我已经开源了</a>。</p>
]]>
        <![CDATA[<hr />

<p>最近一周，实现战斗服务的 Mike 同学又给我提了新需要。</p>

<p>他说，用我之前实现的 <a href="http://blog.codingnow.com/2011/12/dev_note_6.html">sharedb</a> 是可以用来做角色之间的数据交换。但是，如果有一个 buff 的行为会修改角色身上多个属性的值，那么，单单靠这个无法保证线程安全。逻辑上需要多个属性被同时的，原子的，被修改；而不希望在 buff 去修改一组属性的同时，有另外的进程读取这组属性。这样可能造成读到一部分旧版本的属性及另一部分新版本数据。</p>

<p>我们又不想通过 RPC 消息机制来传递这些属性，最终我打算实现一个锁机制，以角色为单位锁定其附属的所有属性，保证读写的原子性。</p>

<p>一开始我把这个问题想的比较复杂。我希望让我们的框架调度器可以了解锁。如果一个写锁被获得时，读锁所在的 Lua Coroutine 可以被挂起。这样一旦写锁被释放，就可以准确的去唤醒相关被挂起的 Coroutine 。这样做涉及实现一个线程安全的队列挂在每个可以被锁的对象上。我想了一整天后，暂时放弃了这个复杂的实现方案。</p>

<p>最终，我决定暂时先实现一个简单的读写自旋锁。不通知框架的调度器，直接在 CPU 一级阻塞。这样对现有代码和接口改动最小。在 Lua 层封装自旋锁的接口时，尽量少允许用户的使用自由。不提供显式的加锁和解锁 api 。而只让传入一小片中间不会被挂起的 Lua 代码运行。</p>

<p>自旋锁依托于 sharedb 工作，让不同的 lua state 间可以获得到同一个角色数据身上的同一把锁，这样就可以方便的同步数据了。</p>

<p>btw, 在读写自旋锁的实现中，gcc 的 <a href="http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html">Built-in functions for atomic memory access</a> 非常好用。</p>
]]>
    </content>
</entry>
<entry>
    <title>pbc 优化</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/pbc_improved.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=760" title="pbc 优化" />
    <id>tag:blog.codingnow.com,2012://1.760</id>
    
    <published>2012-04-26T10:47:02Z</published>
    <updated>2012-04-26T11:03:35Z</updated>
    
    <summary>最近几天优化了一下 pbc 。 这是一个大改动，所以写 blog 记录一下。 首先，我为 rmessage 定制了一个 heap alloc ，在使用 rmessage 解包的时候不再调用系统的 malloc 。而是从一个连续内存 heap 上取用内存。这样在删除 rmessage 对象时也会更快。因为只需要把 heap 回收即可。 当然这样会导致 rmessage 解包时用到的内存增加。对于内存紧张，性能关键部分，我还是推荐 pattern 模式。虽然比较难用，但可以保证时间和空间性能。 另外，我增加了 upb 的 Event-based parsing 模式，见新增接口 pbc_decode 。 不过我认为这个 api 不适合直接在 C 里调用，但是用来做动态语言的...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="lua与虚拟机" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>最近几天优化了一下 <a href="https://github.com/cloudwu/pbc">pbc</a> 。</p>

<p>这是一个大改动，所以写 blog 记录一下。</p>

<p>首先，我为 rmessage 定制了一个 heap alloc ，在使用  rmessage 解包的时候不再调用系统的 malloc 。而是从一个连续内存 heap 上取用内存。这样在删除 rmessage 对象时也会更快。因为只需要把 heap 回收即可。</p>

<p>当然这样会导致 rmessage 解包时用到的内存增加。对于内存紧张，性能关键部分，我还是推荐 pattern 模式。虽然比较难用，但可以保证时间和空间性能。</p>

<p>另外，我增加了 <a href="https://github.com/haberman/upb">upb</a> 的 Event-based parsing 模式，见新增接口 <code>pbc_decode</code> 。</p>

<p>不过我认为这个 api 不适合直接在 C 里调用，但是用来做动态语言的 binding 不错。现在 lua  binding 中的 decode 就改用这个实现了。这样每次解包就把所有项都解出来，而不用附着一个 userdata 。回避了手动调用 <code>close_decoder</code> 的问题。</p>

<p>btw, 根据一个同学使用的反馈，他们大多不主动调用 <code>close_decoder</code> ，而依赖 gc 回收 decode 过程中产生的 C  对象。但是这些 C 对象申请的内存不会通知 lua ，所以 lua 的 gc 触发条件不会及时触发。这使得 pbc 的 lua binding 可能占用大量内存。我这次的修改主要针对这个问题。</p>
]]>
        

    </content>
</entry>
<entry>
    <title>让多个 Lua state 共享一份静态数据</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/lua_multi_states_database.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=759" title="让多个 Lua state 共享一份静态数据" />
    <id>tag:blog.codingnow.com,2012://1.759</id>
    
    <published>2012-04-19T09:34:31Z</published>
    <updated>2012-04-23T13:55:51Z</updated>
    
    <summary>如果你在同一个进程里有多个 lua state , 它们需要共享大量的只读数据, 那么可能就不希望在每个 state 启动的时候都加载和解析一遍这些数据. 所以我们需要一个共享只读数据的方法。 前段时间，我实现了一个 共享内存服务 ，这个可以保证共享内存的安全读写。不过，如果数据是只读的，那么就不需要这么复杂了。 我们只需要把数据加载到一个 lua state 中，其它的同一进程内的 state 通过 C 接口去读数据就可以了。 今天，我做了简单的实现，放在了 github 上。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="lua与虚拟机" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>如果你在同一个进程里有多个 lua state , 它们需要共享大量的只读数据, 那么可能就不希望在每个 state 启动的时候都加载和解析一遍这些数据.</p>

<p>所以我们需要一个共享只读数据的方法。</p>

<p>前段时间，我实现了一个 <a href="http://blog.codingnow.com/2012/02/dev_note_10.html">共享内存服务</a> ，这个可以保证共享内存的安全读写。不过，如果数据是只读的，那么就不需要这么复杂了。</p>

<p>我们只需要把数据加载到一个 lua state 中，其它的同一进程内的 state 通过 C 接口去读数据就可以了。</p>

<p>今天，我做了<a href="https://github.com/cloudwu/lua-db">简单的实现</a>，放在了  github 上。</p>
]]>
        <![CDATA[<p>目前可以支持 nil number boolean function table 的数据交换。</p>

<p>function 交换有一些限制，不可以绑定 upvalue 。是用 string.dump 和 load 实现的。</p>

<p>table 类型返回的其实是一组 key ，需要继续用 get 来读取数据。</p>

<p>关于线程安全：我相信只是读一个 lua state 是线程安全的。</p>

<p>经过 resty 同学提示，由于多线程可能同时改写 database 的 stack ，故而有安全隐患。为了解决这个问题，我预先在 database 的 state 中初始化了 32 个 thread ，这样，不同的 thread 可以用独立的 stack ，应该就线程安全了。</p>

<hr />

<p>4 月 23 日:</p>

<p>旧的版本, 在企图查询一个不存在的 key 的时候, 由于 <code>lua_pushstring</code> 不能保证线程安全。所以我重写了全部的实现，改用 C 实现了 hash 表。这份实现的读过程是线程安全的。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(17) : 策划表格公式处理</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/dev_note_17.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=758" title="开发笔记(17) : 策划表格公式处理" />
    <id>tag:blog.codingnow.com,2012://1.758</id>
    
    <published>2012-04-18T07:00:12Z</published>
    <updated>2012-04-18T07:08:52Z</updated>
    
    <summary>前段时间为策划提供过一些技术上的支持, 设计过一个简单的 DSL 。但随着更多策划加入团队，我觉得这个思路不能很好的贯彻下去。 策划更喜欢通过 excel 表格来表达他心中的数值关系，而不是通过代码语言。我需要找到另一种更切合实际的方案来将策划的想法转换为可以运行的代码。 研究了一下策划历史项目上的 excel 表格后，我归纳了一下其需求。我们需要把问题分步解决，excel 表格到程序可以利用的数据结构是一项工作，而从表达了数据之间联系的结构到代码又是另一项工作。 对于数值运算，有怎样的需求呢？ 我更多看到的是一种声明式的表达，而不是过程式的。比如，策划会定义，“血量”这个属性，实际上是等价于“耐力 * 10”。我们把后者定义为一个公式。 许多表格其实就是在不同的位置表达了这种公式推导关系：一个属性等价于另一些属性组成的表达式。而在运行时，根据人物的一些基础属性，可以通过一系列的公式推导，得到最终的一系列属性值。满足这个需求并不难，读出表格里的对应项，做简单的解析就可以了。（这里涉及到另一个问题，表格里的对应项在哪里，今天暂且不谈） 对于这种声明式表达，程序要做的工作是进行一次拓扑排序，把需要先求值的属性排在前面，有依赖关系的属性求解放在后面。这样就可以转换为过程式的指令。 另一种表格称为查表。其实就是表达一种映射关系。如下表： 法术伤害物理伤害 战士0.51 法师10.5...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>前段时间为策划提供过一些技术上的支持, <a href="http://blog.codingnow.com/2012/01/dev_note_8.html">设计过一个简单的 DSL</a> 。但随着更多策划加入团队，我觉得这个思路不能很好的贯彻下去。</p>

<p>策划更喜欢通过 excel 表格来表达他心中的数值关系，而不是通过代码语言。我需要找到另一种更切合实际的方案来将策划的想法转换为可以运行的代码。</p>

<p>研究了一下策划历史项目上的 excel 表格后，我归纳了一下其需求。我们需要把问题分步解决，excel 表格到程序可以利用的数据结构是一项工作，而从表达了数据之间联系的结构到代码又是另一项工作。</p>

<p>对于数值运算，有怎样的需求呢？</p>

<p>我更多看到的是一种声明式的表达，而不是过程式的。比如，策划会定义，“血量”这个属性，实际上是等价于“耐力 * 10”。我们把后者定义为一个公式。</p>

<p>许多表格其实就是在不同的位置表达了这种公式推导关系：一个属性等价于另一些属性组成的表达式。而在运行时，根据人物的一些基础属性，可以通过一系列的公式推导，得到最终的一系列属性值。满足这个需求并不难，读出表格里的对应项，做简单的解析就可以了。（这里涉及到另一个问题，表格里的对应项在哪里，今天暂且不谈）</p>

<p>对于这种声明式表达，程序要做的工作是进行一次拓扑排序，把需要先求值的属性排在前面，有依赖关系的属性求解放在后面。这样就可以转换为过程式的指令。</p>

<p>另一种表格称为查表。其实就是表达一种映射关系。如下表：</p>

<pre>
<table>
<tr><td></td><td>法术伤害</td><td>物理伤害</td>
<tr><td>战士</td><td>0.5</td><td>1</td>
<tr><td>法师</td><td>1</td><td>0.5</td>
</table>
</pre>
]]>
        <![CDATA[<p>如果公式中提到了“法术伤害”这个属性值时，需要根据“职业”这个属性，去这张表里查到对应项。职业可以是“战士”或“法师”，对应了不同的“法术伤害”值。从 C 语言的角度看，“职业”是一个枚举量，而法术伤害可以定义为一个一维数组，以枚举值为索引，便可以查到对应值。若是动态语言，更适合的方式是做一个字典，性能低一些，但更加灵活。</p>

<p>但无论是表达式求值，还是查表求值，目的都是一个：用一组属性通过某种变换得到新的属性值。每一张表格都提供了一组变换关系，最终可以从一组基础属性推导出各种需求的值来。</p>

<hr />

<p>把策划这个需求解决好并不太难，尤其是对于动态语言来说更为简单。难点在于性能。尤其对于即时战斗的 MMO 来说，每次攻击都需要做一系列的运算，这些运算若是依赖大量的表格推演，性能很难保证。我想，最为彻底的方案是把这些属性数值之间的关系编译成目标机器上的原生代码。为此，我选择了 tcc 这个工具。这样，我只需要用 lua 去一次性生成 C 代码，然后交给 tcc 编译就好了。</p>

<p>ps. <a href="http://repo.or.cz/w/tinycc.git">tcc 最新版本的 git 仓库在这里</a> 。</p>

<p>我首先定义了 lua 和 tcc 交互的接口。最简单的方式是只支持一种数据类型，float 。一个 tcc 内的函数可以接受若干个 float 输入，产生若干个 float 输出。这样的 C 函数的原型看起来是这样：</p>

<pre>
    void func(float input[] , float output[]);
</pre>

<p>交互接口很容易实现，为 lua 写一个 C 扩展即可。</p>

<p>接下来是实现一个 lua 模块利用 tcc 生成需要的运算函数。</p>

<p>这个一个模块，提供一个公式对象，然后可以把一系列的表达式以及查表规则设入对象，最后调用编译方法得到编译好的运算函数。</p>

<pre>
f = require "formula"

test = f()
test:table("属性表", { "战士" , "法师"  } ,
   { ["物理伤害"] = { 1 , 0.5 } , ["法术伤害"] = { 0.5 , 1} })

test:expression("攻击强度", "力量 * 2")
test:expression("血量", "耐力 + 1")
test:expression("伤害", "攻击强度 / 血量 * 物理伤害")

test:lookup("物理伤害","属性表","职业")

test:compile { "伤害" }

print(test { ["力量"] = 2 , ["耐力"] = 3.5 , ["职业"] = "战士" })
</pre>

<p>这里，table 方法可以把一个查询表置入对象。分别是表名，key 表，和数据表。</p>

<p>expression 可以设置属性间的换算表达式。</p>

<p>lookup 可以设置一种查找规则。</p>

<p>最后只需要把基础属性传给 compile 得到的 C 运算函数（传入前做一个替换工作，把字典替换成循序的参数），得到运算结果。</p>

<p>下面看一下，内部生成的 C 代码是怎样的：</p>

<pre class="mtc_block"><span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">float</span> table_1<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span> <span class="c_Symbol def_Symbol">=</span> <span class="def_SymbolStrong def_Symbol"><span class="def_PairStart def_Special">{</span></span><span class="def_NumberDec def_Number">1</span><span class="c_Symbol def_Symbol">,</span><span class="def_NumberFloat def_Number">0.5</span><span class="def_SymbolStrong def_Symbol"><span class="def_PairEnd def_Special">}</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
<span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">void</span> <span class="c_FuncOutline def_Outlined def_Special"><span class="c_KeywordLibFunctions def_FunctionKeyword def_Keyword">main</span></span><span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">(</span></span><span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">float</span> input<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">,</span><span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">float</span> output<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">)</span></span> <span class="def_SymbolStrong def_Symbol"><span class="def_PairStart def_Special">{</span></span>
        <span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">float</span> t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">7</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">0</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>input<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">0</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">2</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>input<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">1</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">4</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>input<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">2</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">1</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">0</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span> <span class="c_Symbol def_Symbol">+</span> <span class="def_NumberDec def_Number">1</span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">3</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">2</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span> <span class="c_Symbol def_Symbol">*</span> <span class="def_NumberDec def_Number">2</span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">5</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>table_1<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">(</span></span><span class="c_KeywordANSI_typenames c_KeywordANSI def_Keyword">int</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">)</span></span>t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">4</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">6</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">3</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span> <span class="c_Symbol def_Symbol">/</span> t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">1</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span> <span class="c_Symbol def_Symbol">*</span> t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">5</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
        output<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">0</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_Symbol def_Symbol">=</span>t<span class="c_Symbol def_Symbol"><span class="def_PairStart def_Special">[</span></span><span class="def_NumberDec def_Number">6</span><span class="c_Symbol def_Symbol"><span class="def_PairEnd def_Special">]</span></span><span class="c_StructureSymbol def_SymbolStrong def_Symbol">;</span>
<span class="def_SymbolStrong def_Symbol"><span class="def_PairEnd def_Special">}</span></span></pre>

<p>一共有三个输入参数，以及一个输出参数，还有三个中间变量。</p>

<p>我们可以把一共 7 个量看作是 7 个寄存器，在分析之前的公式关系时，无非就是做了一下寄存器分配的工作。除了生成这些表达式运算的代码行外，还提取出了查表需要的表列。这里，通过分析输入，可以知道“职业”这一个属性其实是一个枚举量，能用整数表达。那么把“战士”对应到 0 的这个过程其实是在 lua 代码中查表完成的。而 C 函数则接受的是数字而不是字符串了。</p>

<p>最终我们可以计算出“伤害”属性的值，放在 t[6] 中，最后赋予 output[0]。当然，这段 C 代码的生成还可以进一步优化。这些可以留到以后去做了。</p>
]]>
    </content>
</entry>
<entry>
    <title>Lua int64 的支持</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/lua_int64.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=757" title="Lua int64 的支持" />
    <id>tag:blog.codingnow.com,2012://1.757</id>
    
    <published>2012-04-11T11:01:48Z</published>
    <updated>2012-04-11T11:22:07Z</updated>
    
    <summary>虽然今天发了 twitter ，以及向 lua mailling list 里投递了消息，不过想想还是写一篇 blog 记录一下。 Lua 只支持一种 number ，默认是 double 类型。虽然你可以通过修改 luaconf.h 里的定义，把 lua number 改成 int64 。但是为了 int64 类型而放弃浮点数，恐怕不是大多数人想要的。 int64 通常用在 uuid 上，也就是说不需要对其数学运算，只需要可以比较就好了。我以前最喜欢的做法是用 8 bytes 长的 string 来表示一个 int64 。这样，即可以做唯一的 key 用，又不用做复杂的扩展。 在 pbc 的...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="lua与虚拟机" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>虽然今天发了 twitter ，以及向 lua mailling list 里投递了消息，不过想想还是写一篇 blog 记录一下。</p>

<p>Lua 只支持一种 number ，默认是 double 类型。虽然你可以通过修改 luaconf.h 里的定义，把 lua number 改成 int64 。但是为了 int64 类型而放弃浮点数，恐怕不是大多数人想要的。</p>

<p>int64 通常用在 uuid 上，也就是说不需要对其数学运算，只需要可以比较就好了。我以前最喜欢的做法是用 8 bytes 长的 string 来表示一个 int64 。这样，即可以做唯一的 key 用，又不用做复杂的扩展。</p>

<p>在 <a href="http://blog.codingnow.com/2011/12/pbc_lua_binding.html">pbc 的 lua binding 库</a> 中，对 fixed64 类型，我就是这样处理的。</p>
]]>
        <![CDATA[<p>今天遇到新的需求，有同学希望可以在项目中直接处理 64bit 的 timestamp 。这就需要对 int64 做数学运算了。</p>

<p>虽然最终我们去掉了这个需求（使用 32bit 的 timestamp ），但我还是忍不住去想，到底怎样在 lua 中支持 64bit 整数运算最好。</p>

<p>在 luajit 中，是定义了一个 userdata 并重载其运算符完成的。即，你可以用 <code>ffi.cast("int64_t",0)</code> 来构造一个 64bit 的 0 。</p>

<p>姑且不谈 userdata 的额外开销问题，这样做有一个问题就是当 64bit 的 cdata 做 table 的 key 的时候，相同值的 int64 并不是同一个 key 。</p>

<p>我觉得有一个更轻量的方式来解决 int64 支持的问题。那就是在 64 位平台上，我们完全可以用 lightuserdata 无损失的表示一个 int64 。</p>

<p>通过给 lightuserdata 设置 metatable ，我们可以接管它的数据运算。唯一不足的是，比较一个 int64 和普通的 lua  number 是否相等时，lua 不能隐式的做转换。（而大于小于比较则没有问题）</p>

<p><a href="https://github.com/cloudwu/lua-int64">我花了半小时实现了我的想法，放在了 github 上</a> ，有兴趣的同学可以拿去用。</p>

<p>这个库只提供了一个显式的 api ，即构造一个 int64 数字。可以从 lua number 构造，也支持从一个 8 字节宽的小头的字符串来构造。实际在内存储存的是一个 lightuserdata 即一个 64bit 指针（所以这个库不适用于 32 位平台）。你也可以通过 C 接口 <code>lua_pushlightuserdata</code> 来把一个 64bit 整数压入堆栈。</p>

<p>把 int64 转换为普通的 lua number 借用了 # 操作符。</p>

<p>希望这个小东西对你有帮助。</p>
]]>
    </content>
</entry>
<entry>
    <title>如何更准确的网络对时</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/sync_time.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=756" title="如何更准确的网络对时" />
    <id>tag:blog.codingnow.com,2012://1.756</id>
    
    <published>2012-04-09T12:39:28Z</published>
    <updated>2012-04-09T12:39:58Z</updated>
    
    <summary>这个周末又 想到这个问题, 是由另一个问题引起的. 为了模拟复杂的网络环境，我们在内网安装了模拟环境 。 怪物公司同学周末调试客户端时，修改了自己机器的网关，增加了模拟延迟。奇怪的是，他的客户端在切换网关时并没有断开连接。可延迟也果真发生了。 我和他探讨了一下，觉得这个模拟延迟是单向的。当游戏服务器发送数据包回桌面时，由于服务器和他的桌面机在同一个网段，所以 IP 包被直接发回了。TCP 连接也不因为修改了过去的通路和断开。 当然，这种模拟并不是我们想要的。正确的方法应该是在修改网关（指向延迟模拟机器）的同时，也修改桌面机的 IP ，或是给自己机器绑定两个 IP ，使用模拟环境网段的 IP 来重新建立 TCP 连接。或者在模拟网关上做一次 NAT 。反正方法有很多，不展开讨论了。只有正确的模拟双向延迟（或网络颠簸）才好得到接近现实情况的场景。 不过这次错误，引出我另一个思考。如果 TCP 上行和下行延迟差距较大，有没有什么特别糟糕的事情发生呢？我的第一反应是，网络对时不准了。 我们的对时协议一般都遵从这样一个假定。我们的桌面机发送一个数据包到服务器所需要消耗的时间，大约等于服务器发一个数据包回桌面机的时间。这样，我们测试出一个数据包来回的时间，除 2 ，就得到了单程时间。这样就可以根据时间服务送来的服务器时间，把桌面时间和服务器时间基本校准了。 可一旦上下行速度不一致，甚至偏差较大时，这个假定被破坏掉了，时间也无法校准了。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="网络与安全" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>这个周末<a href="http://blog.codingnow.com/2006/04/sync.html">又</a> 想到这个问题, 是由另一个问题引起的.</p>

<p>为了模拟复杂的网络环境，我们<a href="http://www.linuxfoundation.org/collaborate/workgroups/networking/netem">在内网安装了模拟环境</a> 。</p>

<p>怪物公司同学周末调试客户端时，修改了自己机器的网关，增加了模拟延迟。奇怪的是，他的客户端在切换网关时并没有断开连接。可延迟也果真发生了。</p>

<p>我和他探讨了一下，觉得这个模拟延迟是单向的。当游戏服务器发送数据包回桌面时，由于服务器和他的桌面机在同一个网段，所以 IP 包被直接发回了。TCP 连接也不因为修改了过去的通路和断开。</p>

<p>当然，这种模拟并不是我们想要的。正确的方法应该是在修改网关（指向延迟模拟机器）的同时，也修改桌面机的 IP ，或是给自己机器绑定两个 IP ，使用模拟环境网段的 IP 来重新建立 TCP 连接。或者在模拟网关上做一次 NAT 。反正方法有很多，不展开讨论了。只有正确的模拟双向延迟（或网络颠簸）才好得到接近现实情况的场景。</p>

<p>不过这次错误，引出我另一个思考。如果 TCP 上行和下行延迟差距较大，有没有什么特别糟糕的事情发生呢？我的第一反应是，网络对时不准了。</p>

<p>我们的对时协议一般都遵从这样一个假定。我们的桌面机发送一个数据包到服务器所需要消耗的时间，大约等于服务器发一个数据包回桌面机的时间。这样，我们测试出一个数据包来回的时间，除 2 ，就得到了单程时间。这样就可以根据时间服务送来的服务器时间，把桌面时间和服务器时间基本校准了。</p>

<p>可一旦上下行速度不一致，甚至偏差较大时，这个假定被破坏掉了，时间也无法校准了。</p>
]]>
        <![CDATA[<hr />

<p>有没有什么办法可以知道 A 点到 B 点的单程时间呢？如果只有 A 和 B 存在，似乎永远都无法测准。你很难知道 A 到 B 的开销是不是和 B 到 A 的开销相同。除非双程时间特别短（至少有一次特别快），误差就不会超过这个双程时间了。极端情况是，A 到 B 特别慢，而 B 到 A 是瞬间到达的。（有如我前面提到的情况，A 到 B 经过了一个故意的延迟，而 B 到 A 走了局域网的另一条路，瞬间抵达了）</p>

<p>我认为，如果能增加足够多的第三方路径，就能提高这个校对精度。</p>

<p>假定，我们有另外几台服务器，C D E 。A 向其发的包，它们都立刻把包转发到 B ; 反过来，B 发过来的包，也都立刻转到 A 。</p>

<p>那么，我们就可以制造出几条间接连接 A B 的不同路径。</p>

<p>我们假定，互联网上，两个 IP 间的通讯时间，双向延迟接近是常态，而时间不同是例外的话；就可以让对时包走不同的路径从 A 到 B ，可以推算出不同路径的单向时长（假定是双向时长的一半）。</p>

<p>估算出每条路径的延迟后，我们同时从 A 走不同的路径发送校时包到 B 。由于不同路径的延迟不同，B 会先后收到来源于 A 的包。如果认为前面假定成立，那么，相互比对，就可以除掉那些不稳定的路径，最终可以推算出 A 到 B 的直接连接路径上的单程时长了。</p>

<hr />

<p>周末瞎想而已，不必当真 :)</p>
]]>
    </content>
</entry>
<entry>
    <title>Ringbuffer 范例</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/04/mread.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=755" title="Ringbuffer 范例" />
    <id>tag:blog.codingnow.com,2012://1.755</id>
    
    <published>2012-04-06T11:06:05Z</published>
    <updated>2012-04-06T11:46:35Z</updated>
    
    <summary>前段时间谈到了 ringbuffer 在网络通讯中的应用 。有不少朋友写 email 和我探讨其实现细节。 清明节放假，在家闲着无聊，就实现了一个试试。虽然写起来还是挺繁杂的，好在复杂度还在我的可控范围内，基本上也算是完成了。 设想这样一个需求：程序 bind 并listen 一个端口，然后需要处理连接到这个端口上的所有 TCP 连接。当每个连接上要数据过来时，收取这些数据，识别出封包，发送给对应的逻辑层处理。如果数据不完整，则暂时挂起这些数据，直到数据收取完整再行处理。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="网络与安全" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>前段时间谈到了 <a href="http://blog.codingnow.com/2012/02/ring_buffer.html">ringbuffer 在网络通讯中的应用</a> 。有不少朋友写 email 和我探讨其实现细节。</p>

<p>清明节放假，在家闲着无聊，就实现了一个试试。虽然写起来还是挺繁杂的，好在复杂度还在我的可控范围内，基本上也算是完成了。</p>

<p>设想这样一个需求：程序 bind 并listen 一个端口，然后需要处理连接到这个端口上的所有 TCP 连接。当每个连接上要数据过来时，收取这些数据，识别出封包，发送给对应的逻辑层处理。如果数据不完整，则暂时挂起这些数据，直到数据收取完整再行处理。</p>
]]>
        <![CDATA[<p>我写的这个小模块实现了这样一组特性，因为使用了唯一的 ringbuffer 缓存所有的连接，可以保证在程序运行过程中，完全没有额外的内存分配操作。</p>

<p>在编写时，一开始考虑到可能跨平台，想使用 libev, libuv, 或 libevent 来实现。可是仔细思考后，觉得这些库的  callback 模式简直是反人类，完全不符合自然的数据处理流程。使用起来体验非常糟糕。如果考虑到自己做 buffer 管理，想和它们原有的处理框架结合在一起，那实现过程绝对是一个噩梦。</p>

<p>在阅读 redis 的源代码过程中，我发现它并没有实现第三方的连接库，而是自己分别实现了 epoll kqueue select 的处理逻辑。简单清晰。而 epoll 这类 api 原本就相当的简洁，何苦去链接一个近万行的框架库把问题弄复杂呢？</p>

<p>所以，最终我自己定义了一组 API 接口完成需求：</p>

<pre>
struct mread_pool * mread_create(int port , int max , int buffer);
void mread_close(struct mread_pool *m);

int mread_poll(struct mread_pool *m , int timeout);
void * mread_pull(struct mread_pool *m , int size);
void mread_yield(struct mread_pool *m);
int mread_closed(struct mread_pool *m);
int mread_socket(struct mread_pool *m , int index);
</pre>

<p>暂时我不处理数据发送，让用户自己用系统的 send api 来完成，只关注 recv 的处理。这组 api 中，使用 create 来监听一个端口，设置最大同时连接处理数，以及希望分配的 buffer 大小就可以了。</p>

<p>库会为这个端口上接入的连接分配一个子连接号，而没有直接用系统的 socket 句柄。这可以方便应用层的处理。如果你设置了最大连接数为 1024, 那么这个库给你的编号就一定是 [0,1023] 。你可以直接开一个数组来做分发。</p>

<p>poll 函数可以返回当前可以接收的连接编号。可以设置 timeout ，所以有可能返回 -1 表示没有连接可读。</p>

<p>在 poll 之后，pull 函数可以用来收取当前激活的连接上的数据。你可以指定收多少字节。这个函数是原子的，要么返回你要的所有字节，要么一个字节都不给你。</p>

<p>由于库内部管理了接收 buffer ，所以不需要外部分配 buffer 传入。库的内部会识别，如果内部数据是连续的，那么直接返回内部指针；如果不连续，会在 ringbuffer 上重新开一个足够大的空间，拼接好数据返回。</p>

<p>buffer 的有效期一直会到下一次 poll 调用或是 yield 调用。</p>

<p>yield 函数可以帮助你正确的处理逻辑包。这是因为库还帮你做了一件事，如果你不调用 yield ，那么如果你 pull 了多少数据，再下一次 poll 调用后，同一个连接上，会重新 pull 到相同的数据。</p>

<p>举例说，如果你的逻辑包有两个字节的包头表示逻辑长度。你完全可以先 pull 两个字节，根据两个字节的内容做下一次 pull 调用。如果实际数据没有全部收到，你不必理会即可。如果手续收齐，那么调用一次 yield 通知库抛弃之前收取的数据即可。</p>

<p>当 pull 返回空指针，有可能是数据还没有收全，也有可能是连接断开。这时用 closed 这个 api 检测一下即可。</p>

<hr />

<p>写了这么多，一定有同学想找我要源代码了。懒虫们，没问题。我已经提交到 github 上了。你可以从这里拿到 <a href="https://github.com/cloudwu/mread">https://github.com/cloudwu/mread</a> 。</p>

<p>不过，这个只是我的节日娱乐之作。没有经过仔细测试，用在生产环境请务必小心，它几乎是肯定有 bug 的。另外，我只实现了 epoll 的部分，虽然扩展到 kqueue 或是 select / iocp 并不会太困难，不过假日过完了，也没空完善它了。</p>

<p>我由衷的希望有同学有兴趣有能力可以帮我完善这个库，让它更具有实用价值。写 email 给我，我会尽量配合。</p>

<hr />

<p>这个东西的特点是什么呢？</p>

<p>我相信它足够高效，至少从 api 设计上说，可以实现的很高效。</p>

<p>因为它几乎就是直接调用 epoll 这些系统 api 了。而且尽量少调用了系统 api 。从实现上，每次 pull 调用，库都尽量多的读取数据并缓存下来，而不是按用户需求去收数据。</p>

<p>空间占用上，它也一点都不浪费，不会为每个独立的连接单独分配缓存。你大概可以根据你的网卡吞吐量和应用程序能处理的带宽，估算出一个合理的 ringbuffer 总量，那么这个库就应该可以正常工作。</p>

<p>就上一篇谈 ringbuffer 的 blog 我说过，当 ringbuffer 用满，它仅需要踢掉最早残留在系统里的挂起的连接。如果 client 是友好的（不发半个逻辑包），它几乎不会被踢掉。</p>

<p>libevent 这类库设计了一个 callback 框架，让你在每个连接可读时，采用 callback 函数来处理即将收到的数据。和这种用法相比，这个库的处理逻辑更加自然，也不需要你定制这定制那。复杂度被藏在里模块内部。外部接口和 socket api 一样简单易用，甚至更易用。因为它可以帮你保证逻辑包的原子性。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(16) : Timer 和异步事件</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/03/dev_note_16.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=754" title="开发笔记(16) : Timer 和异步事件" />
    <id>tag:blog.codingnow.com,2012://1.754</id>
    
    <published>2012-03-31T07:22:01Z</published>
    <updated>2012-03-31T07:25:42Z</updated>
    
    <summary>这几天，安排新来的王同学做数据持久化工作。一开始他是将 sharedb 里的数据序列化为文本储存的。这步工作做完后，开始动手把数据放到 Redis 数据库中。我们的系统主干由 Lua 构建，所以需要一个 Lua 的 Redis 库。google 来的那份，王同学不满意。三下五除二自己重写了一个。据说把代码量减少到了原来的三分之一（开源计划我正在督促）。唯一的问题是，如果直接采用系统的 socket 库，不能很好的嵌入我们的整个通讯框架。我们的 skynet 全部是通过异步 IO 自己调度的，如果这个数据服务单方面阻塞了进程，会使得别的进程获得不了时间片。 蜗牛同学打算改进 skynet 增加异步 IO 的支持。 我今天在考虑现有的 API 时候，对比原有的 timer 接口和打算新增加的异步 IO 接口，发现它们其实是同一类东西。即，都是一个异步事件。由客户准备好一类请求，绑定一个 session id 。当这个事件发生后，skynet 将这个 session id 推送回来，通知这个事件已经发生。 在用户编写的代码的执行序上，异步 IO 和...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
            <category term="语言与设计" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>这几天，安排新来的王同学做数据持久化工作。一开始他是将 sharedb 里的数据序列化为文本储存的。这步工作做完后，开始动手把数据放到 Redis 数据库中。我们的系统主干由 Lua 构建，所以需要一个 Lua 的 Redis 库。google 来的那份，王同学不满意。三下五除二自己重写了一个。据说把代码量减少到了原来的三分之一（开源计划我正在督促）。唯一的问题是，如果直接采用系统的 socket 库，不能很好的嵌入我们的整个通讯框架。我们的 skynet 全部是通过异步 IO 自己调度的，如果这个数据服务单方面阻塞了进程，会使得别的进程获得不了时间片。</p>

<p>蜗牛同学打算改进 skynet 增加异步 IO 的支持。</p>

<p>我今天在考虑现有的 API 时候，对比原有的 timer 接口和打算新增加的异步 IO 接口，发现它们其实是同一类东西。即，都是一个异步事件。由客户准备好一类请求，绑定一个 session id 。当这个事件发生后，skynet 将这个 session id 推送回来，通知这个事件已经发生。</p>

<p>在用户编写的代码的执行序上，异步 IO 和 RPC 调用一样，虽然底层通过消息驱动回调机制转了一大圈，但主干上的逻辑执行次序是连续的。</p>

<p>受历史影响，我之前在封装 Timer 的时候，受到历史经验的影响，简单的做了个 lua 内 callback 的封装。今天仔细考虑后发现，我们整个系统不应该存在任何显式的回调机制。正确的接口应该保持和异步 IO 一致：</p>
]]>
        <![CDATA[<p>每个独立服务有一组信息包的分发器，外部来的消息会被并发的处理，每条消息是一个独立的执行序，相互不会被阻塞。同时，服务本身有一个主干执行流程，在启动之初就开始执行，可以认为它响应了一个启动消息。btw, 启动消息同时可以看成是热更新的重启消息。</p>

<p>无论是主干执行序还是其它消息的响应函数，它们都可以在里面调用一个叫 sleep 的函数。这其实就是用底层的 timer 回调实现的。调用的时候，当前执行序会被调度器挂起，直到 skynet 计量指定的时间长度后，重新从这个断点继续执行。</p>

<p>这样做，隐藏了 timer callback 的细节，隐藏了异步性。用户也很难主动的并发出多条并行的执行序来，可以减少系统复杂性。</p>

<p>热更新该怎样做呢？</p>

<p>我希望是在更更新时，收到热更新消息后，只是在系统里设置了一个标记。然后在主干代码中，合适的位置去检查这个标记位，体面的结束。这比较像 C 语言中处理中断信号的手法。比简单粗暴的杀掉注册的 timer 更为合理。</p>

<hr />

<p>ps. 前两天碰到了服务相互依赖性的问题。需要主动在启动流程中提示，依赖哪个服务先启动。也就是有个服务启动管理器的服务，响应服务启动的消息，以及提供 RPC 调用，可以在一个服务启动后，返回成功信号。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(15) : 热更新</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/03/dev_note_15.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=753" title="开发笔记(15) : 热更新" />
    <id>tag:blog.codingnow.com,2012://1.753</id>
    
    <published>2012-03-26T14:22:54Z</published>
    <updated>2012-03-26T14:24:24Z</updated>
    
    <summary>这几天我的工作是设计未来游戏服务器的热更新系统。 这部分的工作，我曾经在过去的一个项目中尝试过 。这些工作，在当时一段时间与广州网易其他项目交流时，也对网易其他项目的设计产生过一些影响，之后，也在实战中，各个项目组逐步发展出许多热更新的系统来。 我最近对之前所用到的一些方案，如修改 lua module 的加载策略，增加一些间接层，来达到热更新代码的系统设计做了一些思考。感觉在处理热更新这个问题时，还不够严谨。经过两天的思考，我按我的构思实现了新系统的雏形。 在函数式编程语言中，热更新通常比较容易实现。erlang , lisp 都把热升级做为核心特性之一。函数副作用越小的语言，越容易做热升级：你只需要简单的把新写的函数替换回去就好了。 对于纯粹的请求回应式的服务，做热升级也相对容易。比如大多数 web server ，采用 REST 风格的协议设计的网站，重启服务器，通常对用户都是透明的。中间状态保存在数据库中，改写了代码，把旧的服务停掉，启动新版的服务，很少有出错的。 按我们现在的游戏服务器设计，大多数服务也会遵循这个结构，所以许多底层的子模块简单重启进程就可以了。但和游戏逻辑相关的一些东西却可能要考虑更多东西。 我想把我们的系统分布设计实现，先实现最简单的热更新功能，再逐步完善。如果一开始就指望系统的任何一个部分都可以不停机更新掉老版本的代码是不太现实的，考虑的太多，容易使系统变的过于复杂不可靠。 那么第一步，我们要实现的就仅仅是游戏逻辑有关的代码热更新。而不考虑更新服务器框架有关的模块。我想把这部分称为热修复，而不是热升级。主要用来解决运行时，不停机去修复一些 bug ；而不是在不停机的状态下，更新新版本。在这个阶段，也不考虑可以更新服务间的通讯协议，只考虑更处理这些协议的代码逻辑。 做以上限制后，热更新系统实现起来就非常简单了。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>这几天我的工作是设计未来游戏服务器的热更新系统。</p>

<p>这部分的工作，<a href="http://blog.codingnow.com/2008/03/hot_update.html">我曾经在过去的一个项目中尝试过</a> 。这些工作，在当时一段时间与广州网易其他项目交流时，也对网易其他项目的设计产生过一些影响，之后，也在实战中，各个项目组逐步发展出许多热更新的系统来。</p>

<p>我最近对之前所用到的一些方案，如修改 lua  module 的加载策略，增加一些间接层，来达到热更新代码的系统设计做了一些思考。感觉在处理热更新这个问题时，还不够严谨。经过两天的思考，我按我的构思实现了新系统的雏形。</p>

<p>在函数式编程语言中，热更新通常比较容易实现。erlang , lisp 都把热升级做为核心特性之一。函数副作用越小的语言，越容易做热升级：你只需要简单的把新写的函数替换回去就好了。</p>

<p>对于纯粹的请求回应式的服务，做热升级也相对容易。比如大多数 web server ，采用 REST 风格的协议设计的网站，重启服务器，通常对用户都是透明的。中间状态保存在数据库中，改写了代码，把旧的服务停掉，启动新版的服务，很少有出错的。</p>

<p>按我们现在的游戏服务器设计，大多数服务也会遵循这个结构，所以许多底层的子模块简单重启进程就可以了。但和游戏逻辑相关的一些东西却可能要考虑更多东西。</p>

<p>我想把我们的系统分布设计实现，先实现最简单的热更新功能，再逐步完善。如果一开始就指望系统的任何一个部分都可以不停机更新掉老版本的代码是不太现实的，考虑的太多，容易使系统变的过于复杂不可靠。</p>

<p>那么第一步，我们要实现的就仅仅是游戏逻辑有关的代码热更新。而不考虑更新服务器框架有关的模块。我想把这部分称为热修复，而不是热升级。主要用来解决运行时，不停机去修复一些 bug ；而不是在不停机的状态下，更新新版本。在这个阶段，也不考虑可以更新服务间的通讯协议，只考虑更处理这些协议的代码逻辑。</p>

<p>做以上限制后，热更新系统实现起来就非常简单了。</p>
]]>
        <![CDATA[<p>首先，我们的每个服务就是一个包分发器。主干代码在启动时，将一组组函数按模块加载入框架。框架分发消息包，经由分发器调用消息处理函数处理。热更新其实也是由一个消息来触发的。由于我们用 lua coroutine 的方式来分发每个消息处理包，在大多数情况下，收到热更新请求时的那一刻，之前的消息包都已经处理完毕了。只需要把消息处理函数的分发器重置，重新把新版的消息处理函数塞进去就可以了。但是，也有一些例外。</p>

<p>我们的系统按功能拆分出了许多服务模块，大量依赖 RPC 来协同工作。所以在一个特定的消息包处理中，如果它通过 RPC 调用了另一个模块中的方法，那么这个消息处理函数是被框架挂起的。直接忽略掉这个处理函数的运行是可能有副作用的（如果没有副作用，我们可以简单的把函数参数记录下来，用这些参数重新调用新版函数即可）。那么更合理的方案是等待所有这些挂起的函数运行完毕，再启动热更新流程。</p>

<p>实现起来并不难，在收到热更新请求时，替换掉消息分发器，截流住所有新的服务请求，记录在队列中。过滤出那些之前的 RPC 请求的返回包，交给框架处理。检测到所有旧请求完成后，切换回正常的消息分发器，并用新版本代码更新所有消息处理函数。</p>

<p>当然这只是很理想的状况，是有可能因为同时更新几个服务，而服务间相互有 RPC 请求造成死锁。我的观点是，暂时先不考虑这些非常状况，等遇到问题了再一个个解决，让系统逐渐演化。这并非是不能一开始就设计出没有问题的系统，而是把系统的复杂度维持在一个可控范围内的举措。而且把热更新的设计目的限制在热修复 bug 的范畴，我认为是可以接受的。</p>

<p>关于 timer 这种特殊的相应函数，我更倾向于直接停止所有 timer callback 回调，让重新启动的新版服务的初始化过程重新注册新的 timer ，这样比较干净。</p>

<p>第二个重要点在于状态数据的继承。</p>

<p>我们的模块在初始化时，往往会构造出一些环境表。消息处理过程是可以带状态的，状态信息就储存在这些状态表中。比如前面提到的 agent 的关注列表。</p>

<p>所以我们应该避免直接在初始化代码中创建 table 出来，而应该调用框架提供的 API 创建这些表，并给它们起上名字。通过这些名字，可以在热更新时直接继承这些表，而不用重新创建。</p>

<p>日后的需求可能是更多样的，因为版本更新后，原来的数据结构发生的改变，当发生数据结构改变时，我们需要在热更新的初始化阶段更新这些数据结构。</p>

<p>对于这类问题，我的看法还是一样。先不必在设计实现热更新系统时包含入这些考量，而在真正遇到这个需求时，再仔细考虑如何演化这个系统。有更为实际的需求，比空想更容易评估怎样做的更好。</p>

<p>第三个问题是逻辑层编写时，由于代码规模的需要，分拆出来的子模块代码。</p>

<p>这些子模块可能和消息分发模式无关，单纯解决某些子问题，又不适合拆分成独立服务。</p>

<p>我这次不希望去修改 lua 的模块加载机制。因为从 lua  5.1 到 5.2 的发展路线来看， lua 5.1 的模块加载机制本身就被过度设计了。我想我会单独为我们的系统设计一套简单的适应热更新的模块机制，而不是直接用 lua 基础库里的 require 。从 API 层面就把不可更新的模块和可更新的模块区分开。不可热更新的模块，比如系统框架等，用 lua 基础库里的 require 机制；可以热更新的则使用自己的。</p>

<hr />

<p>今天花了半天时间把以上想法实现，明天尝试合并入主干代码。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(14) : 工作总结及玩家状态广播</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/03/dev_note_14.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=752" title="开发笔记(14) : 工作总结及玩家状态广播" />
    <id>tag:blog.codingnow.com,2012://1.752</id>
    
    <published>2012-03-22T07:29:39Z</published>
    <updated>2012-03-26T14:23:41Z</updated>
    
    <summary>总结一下最近的工作，修改 bug 以及调整以前设计的细节是主要的。因为往往只有在实现时，需求和问题才会暴露出来。 pbc 自从开源以来，收到了公司之外的许多朋友的反馈。因此找到了许多 bug ，并一一修复了。一并感谢那些在不稳定阶段就敢拿去用的同学。我相信 bug 会初步趋进于零的，会越来越放心。 之前的 RPC 框架 经历了很大的修改，几乎已经不是一开始的样子了。简化为主。几乎只实现了简单的一问一答的远程调用。但在易用性上做了更多的工作。coroutine 是非常重要的语言基础设施，所以这个很依赖 lua 。主要就是提取出函数定义的元信息，把远程调用描述成简单的函数调用，把函数的参数以及返回值映射为通讯协议中的消息结构。这部分和整个框架结合较紧，本来想做开源，但不太想的明白怎么分拆出来。 skynet 是由蜗牛同学用 erlang 实现的，原来是很依赖每个服务的 lua 虚拟机来跑具体应用。最近一个月，我们把 lua 虚拟机从 skynet 模块中完全剥离出来了。而服务则仅以 so 的形式挂接到框架中。lua 服务做为一个具体应用存在。这样，独立启动 lua 和 luajit 也是可以的了。这个过程中，因为 lua 的链接问题 ，我们再次调整了 makefile 中的链接策略。完整的读过了 dlopen 的文档，把每个参数都熟悉了一遍，要避免插件式的...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>总结一下最近的工作，修改 bug 以及调整以前设计的细节是主要的。因为往往只有在实现时，需求和问题才会暴露出来。</p>

<p><a href="https://github.com/cloudwu/pbc">pbc</a> 自从开源以来，收到了公司之外的许多朋友的反馈。因此找到了许多 bug ，并一一修复了。一并感谢那些在不稳定阶段就敢拿去用的同学。我相信 bug 会初步趋进于零的，会越来越放心。</p>

<p><a href="http://blog.codingnow.com/2011/12/dev_note_4.html">之前的 RPC 框架</a> 经历了很大的修改，几乎已经不是一开始的样子了。简化为主。几乎只实现了简单的一问一答的远程调用。但在易用性上做了更多的工作。coroutine 是非常重要的语言基础设施，所以这个很依赖 lua 。主要就是提取出函数定义的元信息，把远程调用描述成简单的函数调用，把函数的参数以及返回值映射为通讯协议中的消息结构。这部分和整个框架结合较紧，本来想做开源，但不太想的明白怎么分拆出来。</p>

<p><a href="http://blog.codingnow.com/2012/01/dev_note_7.html">skynet</a> 是由蜗牛同学用 erlang 实现的，原来是很依赖每个服务的 lua 虚拟机来跑具体应用。最近一个月，我们把 lua 虚拟机从 skynet 模块中完全剥离出来了。而服务则仅以 so 的形式挂接到框架中。lua 服务做为一个具体应用存在。这样，独立启动 lua 和 luajit 也是可以的了。这个过程中，因为 <a href="http://blog.codingnow.com/2012/01/lua_link_bug.html">lua 的链接问题</a> ，我们再次调整了 makefile 中的链接策略。完整的读过了 dlopen 的文档，把每个参数都熟悉了一遍，要避免插件式的 so 服务工作时不要再出问题。</p>

<p>上面这个工作的起因之一是，在我们这里实习的同学回学校了，把他在做的 <a href="http://blog.codingnow.com/2011/12/dev_note_6.html">sharedb</a> 的部分工作交接到新同事手里。需要把这个模块整合进 skynet ，然后围绕它来实现后面完整的场景服务。在一开始我实现的时候，因为早于 skynet 的实现，我用到了 zeromq 做一些内部通讯，这次想一并拿掉。由于不想在这个 C 写的底层模块上嵌入 lua 来写服务，所以整理了框架的 C 接口。</p>

<p>sharedb 这一系列的工作中还包括了数据格式的定义，解析，持久化。我们也做成了独立的服务。关于持久化，目前暂时用的文本文件，尚未 <a href="http://blog.codingnow.com/2011/11/dev_note_2.html">整合入 redis</a> 。具体数据储存怎样做，是以后的独立工作可以慢慢来。毕竟在运行期都是直接通过共享内存直接访问的。</p>

<p>下面谈谈对于下面工作的一些思路。主要是场景服务以及一些同步策略。</p>
]]>
        <![CDATA[<p><a href="http://blog.codingnow.com/2011/12/dev_note_5.html">之前的一些想法</a> 差不多废弃了，并没有按照那些想法来实现。</p>

<p>我还是想围绕 <a href="http://blog.codingnow.com/2011/11/dev_note_1.html">agent</a> 来实现功能。为每个 client 部署一个 agent 服务，一对一服务。</p>

<p>agent 在处理完登陆认证，选择角色后（这部分的 client/server 都已经实现完毕），它通知持久化服务加载角色数据。持久化服务从外存中把数据载入 sharedb ，之后，agent 获得了对角色数据的完全读写权。当然我们的读写权是约定的，并不是代码保证的。</p>

<p>之后，这个角色对象，在系统中得到了一个唯一 id 。任何 skynet 上的服务，都可以通过向 sharedb 发起一个 attach 请求，拿到共享对象。任何服务都可以通过共享内存，直接读取角色数据。但是改写数据需要通知 agent 来完成。</p>

<p>场景服务分成两个部分，一是副本管理器，二是地图服务。在角色数据上，记录有角色应该属于的地图。agent 向地图所属的副本管理器查询，得到他所属的地图服务地址。便可以把自己注册到具体地图上。</p>

<p>地图服务管理了所有其中的角色 id ，以及若干 npc 。他的义务在于把让这些 id 对应的 agent 相互了解。但具体逻辑则放在每个 agent 服务上。每个 agent 自己所属进程 attach 其它 id ，可以获取其他对象的状态。</p>

<p>我们会用到刚实现好的 <a href="http://blog.codingnow.com/2012/03/dev_note_13.html">AOI 服务</a> 。每张地图会创建一个（以后会有第 2 个用来驱动 NPC 和怪的行为）AOI 实例用来管理每个 agent 的视野。这个工作是这样完成的：</p>

<p>地图把所有进入的角色 id 加在一个 AOI 实体内，并用固定周期获取 AOI 消息。根据消息里包含的 id 信息，把消息分发给对应的 agent 。agent 获得属于它的 AOI 消息后，分别各自保存一个感兴趣的 id 列表，用来做下面会提到的信息过滤白名单。</p>

<p>我们把 agent 之间通讯用到的消息分为两类。一类是会影响到自身状态修改的消息，比如战斗伤害的计算等等。另一类是外观的变化，比如行走，装备变化等等。后一类消息实际上并不需要 agent 直接处理，agent 并不关心这些外观变化，它需要做的仅仅是把这些消息转发给为之服务的 client ，让玩家机器上看到的对象可以有视觉上的改变就行了。这里涉及到一个流量优化的问题。</p>

<p>一开始我想到的是一个消息队列以及主动查询的策略。就是反其道而行之。角色并不向其他人广播这条消息，而是记录在自己的状态队列里。状态队列是有限固定大小的，每条消息有版本号，并且可以冲刷掉老消息。</p>

<p>这个状态消息队列是用 sharedb 共享出去，其他玩家可以来查询。这样，每个 agent 可以根据其它 agent 的距离远近来设置频率主动查询其它 agent 最近的状态改变。如果太久没有查询而丢失掉一些线索（根据版本），则把角色的全部状态信息全部发送给 client ，而不仅仅是根据消息发送差异。其实全部状态也并不会很多，无非就是位置坐标，动作，以及身上的装备和 buff 而已。假设队列设置的足够大，查询频率不算太低，则比较难丢失线索。</p>

<p>改主动发送为主动查询，好处在于可以减少玩家密集时造成的峰值数据包交换数量。每个 agent 的运行都是公平时间片的。agent 可以根据周围玩家数量决定单个时间片内的查询次数，调节网络流量，也可以根据业务逻辑，适当的忽视一些不太重要的消息。玩家可能会觉得周围的景象变化不太灵敏，但几乎不会有卡的感受。</p>

<p>不过这个方案在考虑具体实现时，我发现了许多实现上的复杂度，暂时放弃了。</p>

<p>现在的做法是利用 agent 的 AOI 对象做一个白名单过滤。每个 agent 发出一个状态消息时，它其实是广播给地图上的所有其他 agent 的。我们在 skynet 层提供了一个高性能的<a href="http://blog.codingnow.com/2012/02/dev_note_11.html">组播服务</a> 。由于 agent 和地图一定存在于一台物理机上，skynet 用 erlang 可以高效的实现组播，甚至没有内存拷贝，只是将数据包的引用传给了每个 agent 。agent 根据包头就可以识别出这是一个状态信息。它检查白名单，发现消息来源不在白名单内（距离很远），就直接丢弃这个包。否则，把这个包转发给 client 。这个过程是不需要解开数据包的。</p>

<p>地图服务的另一项工作是管理所有的 NPC 和怪。我原本希望把单个 NPC 和怪都放到独立的进程（非 os 进程）中，以 agent 的独立地位存在。做过一番估算后，觉得内存开销方面不太现实。所以还是打算由地图统一管理它们。初步的计划是讲怪物的巡逻路径单独管理，把 AI 的状态机则独立出来。怪物是不作为观察者进入 AOI 的。让 agent 在观察到怪后，主动向怪发送消息激活怪的 AI 模块。这样可以在没有人活动的场景上，大大减少怪物 AI 轮询造成的 CPU 压力。这部分的实现以后由新入职的 mike 同学负责实现。这几天我们讨论了好长时间的相关方案。在实现过程中一定还会有许多实现者自己的想法。不过我相信 mike 同学多年的 MMO 制作经验可以很好的解决掉问题。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记 (13) : AOI 服务的设计与实现</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/03/dev_note_13.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=751" title="开发笔记 (13) : AOI 服务的设计与实现" />
    <id>tag:blog.codingnow.com,2012://1.751</id>
    
    <published>2012-03-05T16:39:11Z</published>
    <updated>2012-03-07T13:15:00Z</updated>
    
    <summary>今天例会，梳理了工作计划后，发现要开始实现 AOI 模块了。 所谓 AOI ( Area Of Interest ) ，大致有两个用途。 一则是解决 NPC 的 AI 事件触发问题。游戏场景中有众多的 NPC ，比 PC 大致要多一个数量级。NPC 的 AI 触发条件往往是和其它 NPC 或 PC 距离接近。如果没有 AOI 模块，每个 NPC 都需要遍历场景中其它对象，判断与之距离。这个检索量是非常巨大的（复杂度 O(N*N) ）。一般我们会设计一个 AOI 模块，统一处理，并优化比较次数，当两个对象距离接近时，以消息的形式通知它们。 二则用于减少向 PC 发送的同步消息数量。把离 PC 较远的物体状态变化的消息过滤掉。PC...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>今天例会，梳理了工作计划后，发现要开始实现 AOI 模块了。</p>

<p>所谓 AOI ( Area Of Interest ) ，大致有两个用途。</p>

<p>一则是解决 NPC 的 AI 事件触发问题。游戏场景中有众多的 NPC ，比 PC 大致要多一个数量级。NPC 的 AI 触发条件往往是和其它 NPC 或 PC 距离接近。如果没有 AOI 模块，每个 NPC 都需要遍历场景中其它对象，判断与之距离。这个检索量是非常巨大的（复杂度 O(N*N) ）。一般我们会设计一个 AOI 模块，统一处理，并优化比较次数，当两个对象距离接近时，以消息的形式通知它们。</p>

<p>二则用于减少向 PC 发送的同步消息数量。把离 PC 较远的物体状态变化的消息过滤掉。PC 身上可以带一个附近对象列表，由 AOI 消息来增减这个列表的内容。</p>

<p>在服务器上，我们一般推荐<a href="http://blog.codingnow.com/2008/07/aoi.html">把 AOI 模块做成一个独立服务</a> 。场景模块通知它改变对象的位置信息。AOI 服务则发送 AOI 消息给场景。</p>

<p>AOI 的传统实现方法大致有三种：</p>

<p>第一，也是最苯的方案。直接定期比较所有对象间的关系，发现能够触发 AOI 事件就发送消息。这种方案实现起来相当简洁，几乎不可能有 bug ，可以用来验证服务协议的正确性。在场景中对象不对的情况下其实也是不错的一个方案。如果我们独立出来的话，利用一个单独的核，其实可以定期处理相当大的对象数量。</p>

<p>第二，空间切割监视的方法。把场景划分为等大的格子，在每个格子里树立灯塔。在对象进入或退出格子时，维护每个灯塔上的对象列表。对于每个灯塔还是 O(N * N) 的复杂度，但由于把对象数据量大量降了下来，所以性能要好的多，实现也很容易。缺点是，存储空间不仅和对象数量有关，还和场景大小有关。更浪费内存。且当场景规模大过对象数量规模时，性能还会下降。因为要遍历整个场景。对大地图不太合适。这里还有一些优化技巧，比如可以<a href="http://blog.codingnow.com/2009/09/aoi_watchtower.html">把格子划分为六边形</a> 的。</p>

<p>第三，使用十字链表 (3d 空间则再增加一个链表维度) 保存一系列线段，当线段移动时触发 AOI 事件。算法不展开解释，这个用的很多应该搜的到。优点是可以混用于不同半径的 AOI 区域。</p>

<p>接下来我要来实现这个 AOI 服务了，仔细考虑了一下，我决定<a href="http://blog.codingnow.com/2008/11/aoi_server.html">简化以前做的设计</a> 。</p>

<p>首先是最重要的协议设计。以前我认为，AOI 服务器应该支持对象的增删，可以在对象进入对方的 AOI 区域以及退出 AOI 区域时发出消息。</p>
]]>
        <![CDATA[<hr />

<p>这次我重新思考了一下，觉得是可以简化的。</p>

<p>我只打算支持固定 AOI 半径，下面是我重新设计的协议：</p>

<pre>
typedef void * (*aoi_Alloc)(void *ud, void * ptr, size_t old_sz, size_t new_sz);
typedef void (aoi_Callback)(void *ud, uint32_t id, uint32_t id);

struct aoi_space;

struct aoi_space * aoi_create(aoi_Alloc alloc, void *ud, float radis);
void aoi_release(struct aoi_space *);

// w(atcher) m(arker) d(rop)
void aoi_update(struct aoi_space * space , uint32_t id, const char * mode , float pos[3]);
void aoi_message(struct aoi_space *space, aoi_Callback cb, void *ud);
</pre>

<p>核心只有两条协议，即最后两个 C API 。update 用来更新对象的状态；message 用来获得 aoi 消息。</p>

<p>这次不打算发送离开 AOI 区域的消息，而只会发送进入 AOI 区域的消息。如果对象始终维持在 AOI 区域中，尽量不发送新的消息，不确保这一点，但不会有过于频繁的消息推送。</p>

<p>我把对象分为两类，一类叫观察者 Watcher ，另一类是被观察者 Marker 。一个对象可以同为 Watcher 和 Marker 。在 Marker 进入 Watcher 的 AOI 区域时会触发消息。</p>

<p>对象用 32 位 id 表示；update 的 mode 参数用来表示这个对象是 Watcher 还是 Marker 。形式如 fopen 的 mode 参数。写上 "wm" 表示即为 Watcher 又是 Marker 。mode 还可以传入 "d" 表示 Drop 丢弃掉这个对象。</p>

<p>为什么不需要退出 AOI 区域的消息？我认为，在使用 AOI 服务时，往往只是用来简化比较距离的操作。收到 AOI 消息后，用户可以选择把对象加入自己关心的列表。以后在处理遍历这个列表时，有足够多的机会把不再关心的对象删掉。具体使用时，建议在对象超过两倍 AOI 距离后再取消关注。后面会看到为什么不能在对象离开 AOI 区域后立刻删除，因为那样可能导致不再收到重新进入 AOI 区域的消息。</p>

<p>使用时，定期调用 message 函数。每次调用称为一个 tick 。在这个 tick 里，会把发生的 AOI 事件以回调函数的形式发出来。</p>

<p>以上 API 很容易封装为一个网络服务，方便使用。接下来几天，我将按下面提到的算法实现这个 C 模块，并且开源。 :)</p>

<hr />

<p>这次我不打算用打格子的方法来实现 AOI 模块。经过一番思考，我觉得我找到了一种更好的算法。</p>

<p>我把对象的状态分为三类。</p>

<p>第一类称为静止 (static) 。当一个对象在当前 tick 内没有更新坐标，就认为它是静止的。</p>

<p>第二类称为微动 (shift) 。当一个对象更新了坐标，但新坐标和上个关键坐标距离较小（不超过 AOI 半径的一半）时，我们认为这个对象是微动的。</p>

<p>第三类称为运动 (move) 。当一个对象第一次出现，或它的新坐标离上个关键点距离较大时，我们认为这个对象在运动。进入运动状态的对象会把自己的关键点坐标更新为当前位置。</p>

<p>在 AOI 空间中，我储存了 6 个集合对应 Watcher 和 Marker 的三类状态。一开始，这 6 个集合都是空的。</p>

<p>另外，在空间中还保存有四个集合，属于新增的微动对象 (shift new) 和新增的运动对象 (move new)。</p>

<p>在 update 函数中，新的 id 一定被加入 move new 集合。需要删除 (drop) 的 id 则简单打上 drop 标记。以前提到过的 id ，则查询更新的坐标和旧坐标的距离，更新其状态。如果有状态较之以前有改变，则分别置入 shift new 或 move new 集合。否则什么也不做。</p>

<hr />

<p>message 函数被调用时，处理这些集合，就可以获得 AOI 消息。算法如下：</p>

<p>一，遍历 shift 和 move 集合，如果发现里面的对象当前 tick 没有更新，则把它放到对应的 static 集合中去。如果状态发生改变（从 shift 变成了 move 或从 move 变成了 shift ）则从对应集合删掉，因为它们已经存在于 shift new 或 move new 集合中了。如果对象处于 drop 状态，则减掉对象的引用（对象用引用记数管理）。</p>

<p>二，把 shift new 集合里的对象和 static ,  shift 以及 shift new 集合里的对象逐个比较。如果他们距离较近（两倍 AOI 半径之内），则生成一个热点对，放到热点对列表中。（关于“热点对”，下文会展开解释）。这里的比较指 Watcher 集合和 Marker 集合的交叉比较。比较双方都同为 Watcher 和 Marker 时，注意只比较一次。这很容易实现，因为可以用 id 的大小来做鉴别。</p>

<p>三，将 move new 集合里的对象，和 static , shift , shift new, move , move new 的对象配对一一加入热点列表。不必做距离判断。</p>

<p>四，将 shift new 合并入 shift 集合，move new 合并入 move 集合。</p>

<p>五，在每个 AOI 空间中，都有一个列表，我们称为热点队列表。每个热点对，是我们需要尝试判断是否会触发 AOI 消息的两个 id 对。这个列表里会有哪些 id 对呢？如果你理解了上面几个步骤的处理，就能想到，包含有至少一个运动对象的对象对，距离比较近的微动对象对；没有完全处于静止状态的对象对，也没有距离较远的微动对象对。我们在处理这些热点对时，比较它们和上个 tick 处理时，对象状态是否发生了改变。只要至少有一个对象对象发生了改变，就将这个热点对抛弃。（因为一定有新的正确关联这两个对象的热点对在这个列表中）对于其他有效的热点对，我们比较其中两个对象的距离，当距离小于 AOI 半径时，发送 AOI 消息，并把自己从列表中删除；否则保留在列表中等待下个 tick 处理。</p>

<hr />

<p>以上算法略微有点复杂，但实现起来并不困难。为什么它效率很高呢？</p>

<p>因为，如果对象处于某种速度跨越不大的运动状态中，而 AOI 距离和运动速度相比比较长，那么，运动的对象将比较长时间的停留在微动（ shift ）状态。如果对象停止了运动，则会切换到静止 ( static ) 状态。这两种状态之间的对象，若距离比较远，他们将不会进入热点对，及不会被遍历，也没有比较距离的运算。</p>

<p>只有处于运动状态的对象，会在每个 tick 和其它所有对象做一次比较。而只有少数高速运动或跳转的对象会被打上 move 标记。一般微动 (shift) 的对象则以一个周期被标记为 move 。这些操作是低频的。</p>

<p>完全静止的对象之间的遍历和比较则完全被优化掉了。</p>

<hr />

<p>这篇是一个粗略的想法。大概需要这周余下的时间来实现。在实现过程中，可能会发现一些没有想到的细节，届时再来修改。</p>

<hr />

<p>3 月 7 日:</p>

<p>昨天花了一天实现。发现许多可以简化和改进的地方。我认为可以把 static 以及 shift 都合并到 shift ，简化处理流程。</p>

<p>在实现集合的时候，一开始先用双向链表。而关系对用 hash 表。实作觉得过于复杂，就简化为数组和单向链表了。每次 message 生成的时候用一个 O(n) 的遍历生成几个需要的集合。这样比维护双向链表实现的集合的变化要简洁，性能感觉也不会有太大的损失。</p>

<p><a href="https://github.com/cloudwu/aoi">代码已经放到了 github 上</a>。大致应该是正确可用的。内存分配器可以定制，如果需要优化性能，定制一个内存分配器很重要。因为运动期动态的内存增删都是一针对 <code>pair_list</code> 固定大小的。专门定制可以减少内存占用，减少碎片，加快速度。实现也很简单，做一个固定大小的内存池即可。有空我会实现一个默认的定制内存分配器。</p>

<p>关于测试，今天小 V 同学帮我写一个一个图形化的测试程序，可以直观的看到效果，并核对有没有 AOI 消息的遗漏。大体上是没有问题的。</p>

<p><img alt="aoi.jpg" src="http://blog.codingnow.com/images/aoi.jpg" width="800" height="656" /></p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记(12) : 位置同步策略</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/03/dev_note_12.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=750" title="开发笔记(12) : 位置同步策略" />
    <id>tag:blog.codingnow.com,2012://1.750</id>
    
    <published>2012-03-02T16:07:26Z</published>
    <updated>2012-03-02T17:18:49Z</updated>
    
    <summary>最近两周，陆续有些新同事到岗，或即将到岗。所以我不想过多的再某个需要实现的技术细节上沉浸进去了。至少要等计划的人员齐备，大家都有事情做了以后，个人再好领一块具体事情做。所以属于我个人的代码不多。我主要也就是维护一下前面我自己实现的模块，以及把之前写的一些代码交接给下面具体负责的同学。 哦，这里值得一提的是，我写的 protobuf C 库 慢慢算是可用了。自从提交到 github 上后，有两处 bug 是不认识的网友指出的。当然我自己在用的过程中发现修正的 bug 更多。现在算是基本完善了吧。接下来还会大量使用到，等整个项目做完，应该就基本没问题了。目前主要是用它的 lua binding 。为了用起来更方便，昨天我甚至自己实现了一套 proto 文件的 parser ，作为一个选项，可以不依赖 google 官方提供的工具来编译了。 前两天我们开程序例会。dingdang 主持会议。提到，在工作全面展开前（还有几个程序和策划没有到位），我们最后的一点时间应该把一些技术点解决掉。其中之一就是解决好即时战斗游戏中的位置同步问题。要做到好的操作手感不太容易，至少现在看到的国内的 MMO 没有做的特别让人满意的。我们比较熟悉的网易的产品，天下二，这方面就比较糟糕。 至少要达到 wow 里的水准吧，在网络不稳定，延迟在 200 到 2000 ms 波动时，玩家还要玩的比较舒服。 话说到这里，我想起 6 年前，我们就做过这方面的探索 ，并写了许多代码验证。花了不少的时间。这次怪物公司同学回头又开始看 paper 重新研究。当年和我一起做这块的人不在了，换了一拨人，感觉场景仿佛相识。不过这次技术储备更完备一些，许多工具，Engine 什么的也完整。应该会更顺利吧。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="游戏开发" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>最近两周，陆续有些新同事到岗，或即将到岗。所以我不想过多的再某个需要实现的技术细节上沉浸进去了。至少要等计划的人员齐备，大家都有事情做了以后，个人再好领一块具体事情做。所以属于我个人的代码不多。我主要也就是维护一下前面我自己实现的模块，以及把之前写的一些代码交接给下面具体负责的同学。</p>

<p>哦，这里值得一提的是，我写的 <a href="http://blog.codingnow.com/2011/12/protocol_buffers_for_c.html">protobuf C 库</a> 慢慢算是可用了。自从提交到 github 上后，有两处 bug 是不认识的网友指出的。当然我自己在用的过程中发现修正的 bug 更多。现在算是基本完善了吧。接下来还会大量使用到，等整个项目做完，应该就基本没问题了。目前主要是用它的 <a href="http://blog.codingnow.com/2011/12/pbc_lua_binding.html">lua binding</a> 。为了用起来更方便，昨天我甚至自己实现了一套 proto 文件的 parser ，作为一个选项，可以不依赖 google 官方提供的工具来编译了。</p>

<p>前两天我们开程序例会。dingdang 主持会议。提到，在工作全面展开前（还有几个程序和策划没有到位），我们最后的一点时间应该把一些技术点解决掉。其中之一就是解决好即时战斗游戏中的位置同步问题。要做到好的操作手感不太容易，至少现在看到的国内的 MMO 没有做的特别让人满意的。我们比较熟悉的网易的产品，天下二，这方面就比较糟糕。</p>

<p>至少要达到 wow 里的水准吧，在网络不稳定，延迟在 200 到 2000 ms 波动时，玩家还要玩的比较舒服。</p>

<p>话说到这里，我想起 <a href="http://blog.codingnow.com/2006/04/sync.html">6 年前，我们就做过这方面的探索</a> ，并写了许多代码验证。花了不少的时间。这次怪物公司同学回头又开始看 paper 重新研究。当年和我一起做这块的人不在了，换了一拨人，感觉场景仿佛相识。不过这次技术储备更完备一些，许多工具，Engine 什么的也完整。应该会更顺利吧。</p>

<p>这块 dingdang 认为很重要，如果一开始不做好，以后没完没了的任务堆过来，就不再能回头弄了。天下就是如此。我当年也是这样想的，只不过花了太多时间去弄了，项目开发的节奏控制的不好。下面把我当初理的思路在整理一次，重新列出来，算是个记录。</p>
]]>
        <![CDATA[<hr />

<p>我们需要简化问题，并先解决一个比较小的问题集。把系统搭建起来，这样可以迭代测试。自己玩过一些，才好改进。所谓，快速原型，吃自己的狗粮。那么现阶段集中做不同玩家的位置同步。仅解决这一个问题。把 3d 客户端和服务器搭起来，可以真正的跑起来。我们的 IT ，Aply 同学已经在内网搭建了模拟环境，模拟各种糟糕的网络环境。测试恶劣环境下的表现，我们有比当年更逼真的工具来实现。</p>

<p>不要把问题想的过于复杂，也不要使用太难理解和繁杂的手段来解决问题。不过陷入一些技术细节。比如，让服务器和客户端校对时间就是一个你想钻进去水都不浅的坑。（<a href="http://blog.codingnow.com/2006/05/iaeeoeo.html">见这里</a> ）我们暂时只粗略的构建原型，使用最简洁的方案来做。</p>

<p>首先，我们设计一个最简单的对时协议。即，我们先约定，我们的网络包里的最小时间精度是 10ms ，即 0.01s 。以这个为单位 1 。短于这个时间的都认为是同时发生。</p>

<p>客户端发送一个本地时间量给服务器，服务收到包后，夹带一个服务器时间返回给客户端。当客户端收到这个包后，可以估算出包在路程上经过的时间。同时把本地新时间夹带进去，再次发送给服务器。服务器也可以进一步的了解响应时间。到此为止。</p>

<p>客户端时间和服务器时间具体是什么含义不重要，数值也不必统一。我们简单认为，这个时间值是各自的本地时间就好了。两边分别利用数值计算时差。</p>

<p>由于我们暂时只解决位置同步问题。</p>

<p>首先信任客户端的数据。客户端发送自己的位置坐标和运动矢量（包含有方向和速度）以及当前时间给服务器。</p>

<p>服务器收到后，认为在某一时刻（客户端时间），这个玩家在什么位置，怎样运动的。根据对时求得的时差和估算的延迟，可以预计客户端当前时刻（服务器时间）应该是什么状态（位置以及运动矢量）。把这个信息广播给所有的玩家。</p>

<p>每个玩家收到后，再根据他们之前估算出来的时差以及延迟，得到本地时间当时，所有玩家的状态。</p>

<p>因为玩家运动是连续的。上面得到的状态和他们看到的这些角色的时间状态会有偏差。校准偏差分两种情况讨论。</p>

<p>一种，收到的信息是属于其它玩家的。我们从最新得到的状态信息，预测一段时间之后（比如一秒后的状态），用一条直线运动去修正。即，设想一秒后这个玩家在哪里，然后反推回现在应该用什么速度运动可以在一秒后到达那个地方。</p>

<p>另一种，收到的信息是属于自己的，即服务器认可的自己的状态（并广播给别人了）。这个偏差是由于服务器的预测补偿造成的。为了保持用户的操作手感，对于不太极端的偏差，我们全部不修正，而是依然发送客户端自己操作的位置状态给服务器。服务器那边玩家是处于一种离散的运动状态的。而其他人见到你会再做预测补偿；如果和服务器相差过于剧烈，则直接跳转到服务器认可的新位置。</p>

<p>这里几乎全部相信客户端的行为，以获取最好的操作手感。防止客户端作弊是另外一个话题，也不是不能解决的，但目前不要碰了。</p>

<p>客户端到底以怎样的频率发送那些位置信息给服务器呢？</p>

<p>策略应该是这样的：</p>

<p>每次发送完一个完整的位置信息后，预测服务器看待这个位置信息包一秒后的位置大约在哪里。每次变化做一个累积，一秒内都但不用立刻发送。但每次小的状态改变都和假设的预测位置做一些比较，如果位置偏差比较大，就可以提前发送。否则一直累计到一秒再发送。</p>

<p>这个一秒的周期可以根据实际测试情况来调整。可能一秒太短，也可能过长了。</p>

<p>每次收到服务器发送过来的新的玩家位置信息时，都在里面会找到一个时间戳，表识的包发出的服务器时间。客户端可以验算之前的网络延迟是否正确。如果网络延迟稳定在一个固定值，说明没有问题。但如果延迟值为负数，则说明之前的对时流程中网络不稳定（可能是因为上下行时间偏差比较大造成的，也可能是当时服务器负载很大，造成了较大的内部延迟），造成本地时间和服务器时间的时差计算错误。这个时候重新发起对时流程就好了。</p>

<hr />

<p>ps. 关于争论。我想到我一个老同学<a href="http://www.douban.com/note/203019390/">在 douban 上写的日记</a>中引用 <a href="http://book.douban.com/subject/6121484/">The Idea Hunter</a> 的一句话特别有道理： </p>

<p>when one is about to dismiss the other as naive or sth , ask " what if he is right"?</p>
]]>
    </content>
</entry>
<entry>
    <title>主题论坛的一些想法</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/02/forum.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=749" title="主题论坛的一些想法" />
    <id>tag:blog.codingnow.com,2012://1.749</id>
    
    <published>2012-02-28T13:30:37Z</published>
    <updated>2012-02-28T13:31:29Z</updated>
    
    <summary>虽然现在 twitter google+ facebook （你也可以把前面的产品换成新浪微博，人人）已经成为网上公众信息交流的主流工具了。但论坛这一形式始终有它存在的价值。至少，在 mailling list 无法成为主流的状态下，产品在网上发布，大多还是需要一个类似论坛的形式为用户提供服务的。当然，google groups 本质上是一个邮件列表，它也把自己称为“网上论坛”的。我说的这个东西，应该大体上归类于 forum 。但 forum 这个词大多数中国人拼不清楚，大家更习惯称之为 bbs （我知道 forum 和 bbs 其实是有差别的）。 当年 ROR 正火爆的时候，有人说用 ROR 搭建一个网站只需要几行代码，没有更简单的了。有人回，不，用 Discuz 搭建一个论坛更简单。 以为然。 但是我始终不喜欢 Discuz 形式的论坛，尤其是它之后的发展。过于花哨繁杂了。我更喜欢 douban 小组那样的简单设计。只不过那个设计过于简单，如果单独抽出来做为一个产品，对于我有许多信息过滤的需求无法满足。 对于为某特定产品服务的论坛，比如为特定网络游戏的用户服务的论坛，我构想的形式大约是这样的。 首先不需要分板块。对于集中话题，按时间整理出信息流就足够了。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="我爱折腾" />
            <category term="杂记" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>虽然现在 twitter google+ facebook （你也可以把前面的产品换成新浪微博，人人）已经成为网上公众信息交流的主流工具了。但论坛这一形式始终有它存在的价值。至少，在 mailling list 无法成为主流的状态下，产品在网上发布，大多还是需要一个类似论坛的形式为用户提供服务的。当然，google groups 本质上是一个邮件列表，它也把自己称为“网上论坛”的。我说的这个东西，应该大体上归类于 forum 。但 forum 这个词大多数中国人拼不清楚，大家更习惯称之为 bbs （我知道 forum 和 bbs 其实是有差别的）。</p>

<p>当年 ROR 正火爆的时候，有人说用 ROR 搭建一个网站只需要几行代码，没有更简单的了。有人回，不，用 Discuz 搭建一个论坛更简单。</p>

<p>以为然。</p>

<p>但是我始终不喜欢 Discuz 形式的论坛，尤其是它之后的发展。过于花哨繁杂了。我更喜欢 douban 小组那样的简单设计。只不过那个设计过于简单，如果单独抽出来做为一个产品，对于我有许多信息过滤的需求无法满足。</p>

<p>对于为某特定产品服务的论坛，比如为特定网络游戏的用户服务的论坛，我构想的形式大约是这样的。</p>

<p>首先不需要分板块。对于集中话题，按时间整理出信息流就足够了。</p>
]]>
        <![CDATA[<p>twitter 式的，一条条的信息，不分主题和回复的信息流又过于嘈杂。所以还是需要有主贴和回复之分的。由于论坛是集中的特定主题，所以不需要转发（share）的功能。当然，需要有把老贴精彩贴筛选意义的顶贴(+1) 功能下面会再提到。</p>

<p>对于特定需求来说，我们往往需要把帖子分成几个大类，比如新手帮助，bug 报告，玩家经验，帮会活动等等。传统意义上的论坛是用板块区分的，这对用户过滤出自己需要的信息非常有意义。如果取消掉板块（我不想展开谈分板块的负面作用），那么最直接的功能补偿就是打 TAG 。</p>

<p>TAG 等于是作者对帖子分类的建议，其实和分板块是等价的。读者选择板块阅读其实就是选择了特定的 TAG 做过滤。TAG 的不同在于同一个帖子可以同时拥有不同的 TAG ，并且用户还可以自由的添加新 TAG 。这可以参考 douban 对读书，电影的分类。</p>

<p>我设想，系统可以预设一些 TAG ，供用户写完帖子后选择。当然对于新手，最好的方式是减少他的选择。新人的帖子可以自动 TAG 上新手标签。对于有一定经验的用户，则可以允许（默认）他们在自己的 timeline 里过滤掉新手 TAG 的帖子。这对垃圾广告信息过滤会很很大帮助。</p>

<p>管理员则有权限修改、添加、删除已有帖子的 TAG ，这样，可以把更有价值的帖子展现给需要阅读它们的读者。这里提到管理员，而不是由用户自发构建，是因为这种特定主题下的论坛，我觉得更适合有组织的管理信息。</p>

<p>用户的权利是过滤。他可以不看自己不想看的帖子。</p>

<p>任何自己创作和回复过的帖子，都任何和参与者相关。那么对帖子的任何修改，都应该提到该用户的信息流顶。即，你写的帖子，你回复过的帖子，总会按时间次序排到你打开的页面最上方。当然，你可以通过主动关注这个帖子达到同样的效果。反之，这个帖子一定随时间沉没。你也可以通过主动点忽略（google+ 里叫 Mute）来取消你对特定贴的关注。</p>

<p>用户可以收藏他觉得有价值的帖子，收藏不等于关注。这样一个收藏列表相当于一个私人 TAG ，当用户公开他的 TAG 时，这可以认为是一个信息整理的列表。每个用户都可以把一些他觉得有价值的 TAG 放在他的主界面里方便操作。这些 TAG 就是一个个过滤器。你可以认为这就是 google reader 的 share 功能。而不需要用拙劣的顶贴操作来影响其他人的阅读。</p>

<p>特别提出来一说的是，管理员的收藏 TAG 可以叫做精华贴，以及公告贴。公告自然是强制所有人关注的。</p>

<hr />

<p>我设想的这个系统界面应该是整洁如 douban 小组或 google+ 的信息流的。左边有一系列的默认 TAG 可以开关。右边可以有一些推荐的 TAG 供加入。中间是信息流。按时间排序你关注的 TAG 上的信息流。对于老贴的回复，应该只显示最后没有查看过的几条回复信息，其余的老信息以及太多没有查看的后续新信息都被折叠起来了。帖子不必有标题，但长贴并不全部展开。可以贴图，但不支持图文混排。可以获得单贴的独立链接方便传播到论坛之外的地方。</p>

<p>帖子的正文只支持有限的 markdown 语法，和少量的表情符号。可以用 @ 提到论坛中的其他用户，但文本中的 @名字 并不会直接被翻译成对用户的链接，而只有在输入框中主动按下 @ 后，才会列出提到的人的列表（见 google +）。@ 一个人主要的目的是提起此人的注意，把该贴注入该用户的信息流前端。无论他是否关注了当前贴。</p>

<p>follow 和 unfollow 具体某人在这个特定系统中没有太大意义。这是一个论坛而不是微博。但 follow 此人的收藏是有意义的。</p>

<p>如果有人利用这个系统发布连载小说的话，还需要允许用户对特定 TAG 做逆序排列。这是我对在各种 blog 系统，论坛系统上做连载的最大怨念之一了 :D</p>

<hr />

<p>以上是零碎想到的一些想法，很多细节不太完整，也没有仔细整理。我真心希望未来可以看到类似这样的论坛出现。</p>
]]>
    </content>
</entry>
<entry>
    <title>开发笔记 (11) : 组播服务</title>
    <link rel="alternate" type="text/html" href="http://blog.codingnow.com/2012/02/dev_note_11.html" />
    <link rel="service.edit" type="application/atom+xml" href="http://linode.codingnow.com/cgi-bin/mt/mt-atom.cgi/weblog/blog_id=1/entry_id=748" title="开发笔记 (11) : 组播服务" />
    <id>tag:blog.codingnow.com,2012://1.748</id>
    
    <published>2012-02-21T13:17:26Z</published>
    <updated>2012-02-21T13:18:38Z</updated>
    
    <summary>最近一口气招了 5 个程序员, 在他们没有到岗之前，我不想把自己过多陷入游戏实现的细节上面。所以，除了维护一些前面的代码，陆续发现和修复一些 bug 外，我计划再完善一些基础设施的设计。这些基础模块暂时可以没有，如果实现好了又可以直接加入现有系统。这样比较利于后面的工作划分。 其中之一是服务器组播的模块。 先回顾一下前面提到的服务器架构（skynet ），它可以解决各个服务节点的命名问题，以及把消息从一个节点发送到另一个节点的能力。在不改变接口协议的前提下，最简单实现组播的方法就是把一个组命名为一个节点，由这个节点负责群法消息。 其工作原理类似于 UDP 的局域网组播。我还在网易时，实现过另一个类似的服务，见这篇 blog 。这次重新在另一个系统结构上做，有稍许不同。...</summary>
    <author>
        <name>云风</name>
        <uri>http://www.codingnow.com</uri>
    </author>
            <category term="技术" />
    
    <content type="html" xml:lang="en" xml:base="http://blog.codingnow.com/">
        <![CDATA[<p>最近一口气招了 5 个程序员, 在他们没有到岗之前，我不想把自己过多陷入游戏实现的细节上面。所以，除了维护一些前面的代码，陆续发现和修复一些 bug 外，我计划再完善一些基础设施的设计。这些基础模块暂时可以没有，如果实现好了又可以直接加入现有系统。这样比较利于后面的工作划分。</p>

<p>其中之一是服务器组播的模块。</p>

<p>先回顾一下前面提到的服务器架构（<a href="http://blog.codingnow.com/2012/01/dev_note_7.html">skynet</a> ），它可以解决各个服务节点的命名问题，以及把消息从一个节点发送到另一个节点的能力。在不改变接口协议的前提下，最简单实现组播的方法就是把一个组命名为一个节点，由这个节点负责群法消息。</p>

<p>其工作原理类似于 UDP 的局域网组播。我还在网易时，实现过另一个类似的服务，<a href="http://blog.codingnow.com/2007/03/multicast.html">见这篇 blog</a> 。这次重新在另一个系统结构上做，有稍许不同。</p>
]]>
        <![CDATA[<p>主要的问题在于，组的管理（加入，退出组）和组本身如何区分。</p>

<p>按道理，组的管理和组播本来依赖同一份数据，即组员列表。系统构架最好是实现为表里如一。也就是说，接口上看起来是怎样切分功能的，那么实际功能就怎样物理上隔离比较好。那么，组播消息和组管理，最好看起来就在一个组的节点上。</p>

<p>但这样，会把“组”变成一个于单点不同的对象。必须对网络框架的 API 做修改。我首先放弃了这种方案。</p>

<p>另一个自然的方案是，把所有组的管理放在一起，由一个具名服务来提供。任何一个节点可以向这个组管理节点发送组管理的请求。比如创建一个新的组，并且把若干节点加入这个组里。并给这个组取一个名字。以后向这个特定名字的节点发送的消息，均组播给所有组员。</p>

<p>这样做比较干净，对组发消息可以和对单节点发消息一致。但组管理服务器和组本身看起来是两个名字。如果遵循表里如一的原则，它们的实现也最好在两个节点上。可是用现有的 skynet 接口，它们两者之间很难通讯。</p>

<p>蜗牛同学说服我，组播将是一个重要的基础设施，可以做在底层，成为系统的一部分。那么脏一点也无所谓了。就是说，表面上看起来它是一个独立服务，实际上是系统的一部分。我接受这个建议，这样就不用纠结于怎么把这个东西设计的更好的问题上了。反正不改动之前的设计，额外加这么一块最不影响开发进度了。</p>

<p>有了这样一个基础服务，下面做场景服务、队伍 API 、聊天服务，都会简单很多。</p>

<p>在没有把这个基础服务在通讯框架上实现出来之前，暂时先在 lua 层的库里面做个钩子，截获这些组播相关的调用，由每个节点自行群发消息也很容易模拟出来。</p>
]]>
    </content>
</entry>

</feed> 


