« May 2011 | Main | July 2011 »

June 27, 2011

DNS 隧道

今天在 greader 上看到这么一篇,用 DNS 隧道实现免费上网。我的思绪猛然回到去年去新西兰度假的日子。

有那么几天,我们在惠灵顿的海边山顶租了个屋子,一切都很舒服,但是不能上网。甚至于附近连 wifi 信号都收不到,想"借用"一下别人的 wifi 热点都不成。我顶着海边的狂风在院子里竖起天线,捕捉着周围微弱的信号,最终未果。然后转战屋里的有线电视。我发现和国内的有线电视一样,机顶盒是接有网线的。也就是说,物理上,存在一条链路接入了互联网。但是我插上电脑后,发现 ip 包根本发不出去。不过,好似有个 DNS 服务是可以用的。

当时也没多想,只是觉得有办法可以利用一下。不过隔天就搬走了,没有深入下去。今天回味一下,感觉的确可以利用 DNS 服务和外部建立连接。当然,一开始就需要在外界把接应的程序程序搭建好。

原理很简单,你在做 DNS 查询的时候,如果查的域名在 DNS 服务器本机的 cache 中没有,它就会去互联网上查询,最终把结果返回给你。如果你在互联网上有台定制的服务器。只要依靠 DNS 的这层约定,就可以交换数据包了。从 DNS 协议上看,你是在一次次的查询某个特定域名,并得到解析结果。但实际上,你在和外部通讯。你没有直接连到局域网外的机器,因为网关不会转发你的 IP 包出去。但局域网上的 DNS 服务器帮你做了中转。这就是 DNS Tunnel 了。

我觉得有趣,就按文章所述做了试验。

这需要一个自己的域名,一台运行在互联网上的机器可以自由安装软件。这些都有。那个假的 DNS 是由 OzymanDNS 提供的。不过今天我搜到的 OzymanDNS 网站上已经没有了下载包。好在 google 很强大,这个 2004 年编写的小 perl 程序,并不难找到。

不过我反复试验也没有成功,按文章所述的,利用 ssh 在本机上的 proxycommand 我怎么都无法通过 dnstunnel 连上主机。估计是我哪里配置的不对吧。不过接下来 google 到了更好的选择。那就是 iodine 这个小玩意。非常简单的就装好了。第一次运行发现找不到 tun 设备。看来需要手工建一个。在 linux 上可以自己来: mkdir /dev/net ; mknod /dev/net/tun c 10 200 这样即可。

windows 版麻烦一些,需要装软件。安装个 openvpn 包里的 TAP driver 就行了。

然后利用 iodine 可以利用 dns tunnel 建立起虚拟网。接下来可以方便的用 ssh -D 建起 socks5 了。不过我觉得这个都是多余的。为了节约 dns tunnel 上的带宽,我直接建了个 socks5 服务器。就是用的我前段写的一篇 blog 中提到的 ssocks 。同样需要修改一下,把监听端口绑定在 10.0.0.1 上。

最后,居然没遇到什么麻烦就接通了。我想我所在网络出口上的 dns 服务器肯定觉得很疼。这真是一个超级淡疼的 tunnel 方案啊。嗯,可以留作日后翻墙的备用方案,以备不时之需。

ps. 好想知道到处都有的诸如 CMCC 的 wifi 热点能不能用。下次到商业区别忘记试试看。

June 24, 2011

使用 luajit 的 ffi 绑定 zeromq

最近 Lua 社区非常活跃。6 月 22 日发布了 Lua 5.2.0 (beta-rc2) 。今天(6 月 24 日) 发布了 LuaJIT-2.0.0-beta8 。

虽然 luajit 和 lua 5.2 还有点小矛盾,luajit 没有完全支持 lua 5.2 的迹象。不过,这些对 Lua 社区都是好消息啦。可能对于 lua 用户会有点小纠结,到底是追随官方的 5.2 版呢,还是去用性能更好的 luajit2 。我比较在意性能,暂时先投靠 luajit 了。反正和 5.2 区别也不大。更重要的是,luajit2 提供的 ffi 库相当之好用,极大的减少了我们写 C 库的 lua binding 的负担。从某种角度可以看到另一个问题,为基础设施模块设计出良好的 C 接口(而不是 C++ 的)是多么的重要。

zeromq 是用 C++ 实现的,但它提供的是简洁纯粹的 C 接口。这让它相当利于 binding 到其它语言中使用。之前,已经有了成熟的 lua-zmq 可供使用。它分别实现了 ffi 和不带 ffi 的版本。不过也正因为此,封装层包裹的很淡疼。如果只支持 ffi 版本的话,其实这个工作可以做的非常简洁。

出于实践 luajit ffi 库的目的,也为了让这部分代码看起来清爽一点。我花了半个下午自己封装了一下 zeromq 。所用时间比在 windows 下配置安装那个现成的 lua-zmq 所用时间看起来更少(不需要装 msys ,cmake 等等淡疼的玩意)。谁再下面留言说不要重复造轮子了,我也不打算跟它急了。吵架的时间都比写代码时间长。我们从来不会把写一遍 hello world 看成重新制造轮子不是么?使用 ffi 去 binding C 库实在是太容易了,不比写 hello world 更复杂。

为了鼓励各位同学自己造轮子,我就不把我的实现完整列出来了。希望在 lua 下使用 zeromq 却懒的学习的同学请自己下载上面提到 binding 库好了。

说说我的体会:

zeromq 在 windows 下用 gcc 编译挺麻烦的,我是找一个装了 vc 的同学帮我利用官方的 vs 工程编译出的 dll 文件使用。因为都是 C 接口,所以可以直接 link 到 gcc 项目里。

zeromq 的 api 设计的很精巧合理。总数量扳着手指都数的过来。不过我们对其做适当改造会做起来更轻松一点。

比如:

void *zmq_init (int io_threads);

它返回的是一个 context 对象,出于 C API 设计方便,所以使用的是 void * 。

后面的

void *zmq_socket (void *context, int type);

接受参数正是这个 context 对象,并返回一个 socket 对象,这里依然是 void * 。

我做了一点小修改,把这两个 api 改为了:

    typedef struct {} context;

    context *zmq_init (int io_threads);

    typedef struct {} socket;

    socket *zmq_socket (context *context, int type);

就是说,给它们起了专门的类型名。这样做有什么好处呢?在 lua 代码中,我们可以利用 ffi.metatype 给它们加方法了。虽然 lua 本身的 table 也可以有 metatable 。但利用 ffi.metatype 可以少一个间接层,性能更好,代码更简洁。

我是这样写的:

local context = {}

function init(io_threads)
    return newobj(libzmq.zmq_init(io_threads or 1))
end

function context:term()
    return check(libzmq.zmq_term(self))
end

-- Socket types
PAIR = 0
PUB = 1
SUB = 2
REQ = 3
REP = 4
DEALER = 5
ROUTER = 6
PULL = 7
PUSH = 8
XPUB = 9
XSUB = 10

local socket = {}

function context:socket(type)
    return newobj(libzmq.zmq_socket(self, type))
end

ffi.metatype("context", { __index = context } )

这里 newobj 函数检查返回值是不是 NULL ,如果返回空指针,就利用 strerror 取得错误信息。

local function strerror()
    return ffi.string(libzmq.zmq_strerror(libzmq.zmq_errno()))
end

local function newobj(obj)
    if ffi.cast("void *",obj) > nil then
        return obj
    else
        return false , strerror()
    end
end

我在 ffi 文档中没有找到明确的,判断一个指针是不是 NULL 的方法,琢磨了一下,应该是用 ffi.cast("void *",obj) > nil 不知道是不是惯例。

msg 对象我做的时候保留了完全一致的 C api ,但实际在高层使用的时候不太方便,所以做了进一步的 lua 层封装。类似这样的:

function socket:send_string(str)
    local m = msg(str)
    local err = self:send(m)
    m:close()
    return err
end

function socket:recv_string()
    local m = msg()
    local err = self:recv(m)
    if err == nil then
        local ret = m:tostring()
        m:close()
        return ret
    else
        return false, err       
    end
end

不过我认为性能会有点小问题,如果用 lua 做比较底层的设施,我认为还是用更直接的 msg api 比较好。我想,zeromq 的应用场合偏中低层。应该能够定义出明确需求。使用 luajit 开发,我们在这个层次上应多从 C 的角度去思考(良好使用的 luajit 可以达到 C 的性能级别,但需要多理解和推敲,jit 也不是免费的午餐)。做出更好的封装后,再提供更高层次的接口供上层调用。不大有必要在下面的层次考虑便利性。

setsockopt 和 getsockopt 从 C 接口的角度上看,提供了足够多的灵活性。但表现为 lua 的 api 我认为不宜照搬。因为 C 接口不宜扩展,但 lua 却不一样。这个最好还是根据需要,封装更具体的功能。这些也不是性能敏感的接口,可以多做一些类型检查。


最后正经点说,没有全部公开源代码,其实是因为我觉得根据我的个人需求,我希望将 zeromq 和 google protobuf 做更紧密的结合。放在一起做成一个完整的功能模块。我之前为 lua 定制了一个 protobuf 库 同样也没有开源,也是因为我认为其功能不完整。

所以更进一步的工作是把它们做深度的整合。

ps. 真的想要我写的这个玩意的同学(虽然我觉得也就是 2 小时的工作而已),可以私下找我要。

June 10, 2011

传统 MMORPG 通讯模式实现的一点想法

既然 MMORPG 都有千篇一律同质化的趋势,好歹我们技术人员也应该总结出点东西来,新项目开发可以用现成的模式。

一般来说,MMORPG 服务器要解决的问题无非是,同步玩家的位置,状态,把这些信息广播出去(细分的话,有非战斗环境和战斗环境);需要建立一个聊天服务,供玩家文字交流;有一个信息发布渠道;有任务 NPC 和玩家一对一交流;玩家调整自己的装备(也可以看成是和一特定 NPC 交流)。

以上,我们可以看到几个基本需求是可以正交分解出来,并有不同的实现方式的。

同步玩家的状态,基本上是由玩家自己不断提交自己的最新状态,然后由服务器把这些信息广播给其他人。

聊天服务比较成熟,基本上就是玩家订阅聊天频道,并按权限向频道内发布信息。

信息发布可以看成是一个特殊的聊天频道。

任务 NPC 或是整理自己装备,都类似于向特定节点做请求等待回复。

我经历的几个 MMO 项目都实现的比较简单,无论是 server 还是 client ,都是跑一个消息循环,对每个到来的消息包进行处理。通常都是一个 timer 消息源和一个网络包消息源,共同组成最终的消息源,主程序是标准的消息循环分发结构。

但对比前面的需求,可以看到,这些方式略显简陋。最后能再此基础上再做一个层次的封装工作,让这些不同的需求可以分开处理。更方便的解决一些有状态的需求,或是能够对信息处理进行优先级排序。

最近一段时间,我受 zeromq 的设计思想影响较多。对于未来的项目设计方法,我有一些想法需要梳理一下。

玩家 Client 和游戏 Server 方面,我还是倾向于采用单一的 TCP 连接。但是希望再其上加一个层次。可以在单一 TCP 连接上模拟多个通讯信道。实现 zeromq 提出的几个模式(的变形)。

在我看来,MMORPG 的服务器端,可以看成是若干服务的集合。那么这些服务无论布置在哪里,我都希望他们是在一个逻辑上的网络中。玩家通过网关也接入这个网络,并可以根据授权来使用网络上的服务。

抛开玩家登陆(注册)流程不谈。典型的场景是玩家在虚拟场景中漫步。这可以看成是 Client 订阅一个指定场景的环境。订阅本身会先鉴权,服务器会查阅玩家是否有权限订阅(玩家是否在这个场景中)。场景会不断的发布场景中每个玩家,NPC 的动态。订阅者将不断的获得这些信息。由于性能问题,我们可以把不同类别的信息放在不同的订阅点上。订阅可以按需进行。

玩家会选择向特定场景 PUSH 自己的状态数据,按 zeromq 的模式,这应该走另一个信道。

每个可对话的 NPC 都可以看成是一个特定服务。玩家向它建立连接,然后以 Request/Reply 模式工作。这样可以方便完成带状态的任务。同样,建立连接的过程可以引入鉴权(比如说检查玩家是否在 NPC 附近等)。当然,NPC 同样需要订阅场景,当场景中和他交流的玩家跑开后,可以自动切断信道。

战斗部分的处理可以独立。以即时战斗为例,战斗计算服务订阅场景中对象的位置和动作。计算他们的动作的后果,得到每次攻击的伤害值。它自己是一个信息订阅点,玩家通过订阅战斗服务,可以了解场景中每个个体受伤害的情况。

同样,某些增值服务,比如自动寻路,都可以独立出来实现。


只是一些想法,希望整理出来后,可以有更多启发。对人也对己。