Silly多端口监听支持

前两天在编写一个新服务器程序时才意识到,多台不同类型的服务器相互通信,服务器仅监听一个端口号是非常不方便的,比如服务器集群中的ctrl_server需要接受多种不同的服务器建立链接,让所有链接去涌入同一个端口显然是不明智的,这时候就需要监听多个不同的端口。

让我纠结了两天的问题是到底该以怎么样的方式增加多端口支持。

首先先到的是,去掉配置文件中port的支持,增加一个listen API让lua逻辑自己去指定监听哪几个端口号,但是有一个明显的问题,多个worker现在启动时加载同一套lua代码,一个worker监听成功就会导致,其他worker失败。

现在面临着两个旋择,一个就是让配置文件可以没个worker指定一个bootstrap,但这样会有一个问题,多个worker并不能通信,有可能两个端口的命令需要操作一块共享的数据,因此并不能实现。

还有一个让lua逻辑判断只有workid为零时才真的去监听,其他worker仅仅注册一下相关端口的处理函数即可。但总感觉这种设计有点bad taste的感觉。

还有一种方式就是让silly底层可以支持对一个端口多次监听,但如果lua逻辑是在刚启动就进行端口监听(大多数情况也的确如此),那么多个worker同时调用listen api时就要处理并发问题,这样事情就稍微变得复杂一点了。

为了简化设计,我修改了为lua提供的listen API语义,此api仅仅向socket注册不同端口号的处理函数并不实际进行监听操作,silly根据配置文件中配置的端口号组进行监听,当从某个端口号的listen fd accept出socket向socket.lua发消息时,将这个socket来源于哪个监听端口号一并发出。

这样socket.lua就可以根据端口号来为这个socket fd选择合适的处理函数。

为了避免lua中listen的端口号和配置文件指定的端口号不同的错误,silly的config文件在指定端口号组时需要为每个端口号起一个名字,在lua代码中listen传入的参数是port的名字,而并非是数字。


11月4日补充:

如上文中的ctrl_server即需要接受client请求又需要与其他server进行通信。

client与server之间的通信是不被信任的,所以所有的通信数据包需要进行加密处理,而server与server之间的通信是可以相互信任的,所以都是明文发送。因此一般的服务器集架设过程中会为每台服务器配置2块网卡,一块网卡接入公网为client服务器,另一块网卡用于与其他服务器组成一个局域网。

这样做可以不但可以增加server之间通信的安全性,还可以提高server之间通信的效率。

仔细回忆一两台计算机通过网络通信的流程。1台电脑发出数据包后, 数据包首先到达网关,网关根据去目的ip找到相应的计算机,然后将数据包转到给相应的计算机。

如果server之间使用公网ip进行通信,那么就必须要经过电信的网关,这样数据包不但会有被拦截的风险,还会增加不必要的开销。毕竟相比局域网的网关来说,电信的网关实在太远了。

为了防止有恶意链接从公网接入开放给其他服务器进行通信的接口, 在listen时必须能够支持仅对某块网卡上的特定端口进行监听。

因此silly除了要增加多端口监听支持外, 还需要增加对指针ip地址的某端口进行监听。当我们需要针对任何网卡进行监听时,将ip地址设为0.0.0.0即可表示INADDR_ANY.

使用多态来做到open-close

自从看了设计模式了解到open-close原则后, 我在写代码时都是尽量遵循着open-close原则来进行编码。

而面向对象中的多态在做到open-close原则中起到不可忽略的作用。

一般在设计之初会先抽象出一个interface(也就是C++中的纯虚类), 这个interface中的函数接口一定是要仔细考量的,因为这关系到所有子类的实现。

然后根据具体情况去继承并实现interface,当我们新增功能时,仅仅重新继承一下interface生成一个新的类即可, 在一般情况下并不需要动到之前运行良好的类。当然不论你interface定义的有多么好, 在最后新增需求时都可能不会完全满足,然而这种情况下一般只需对interface增加函数即可, 对之前运行良好的代码也并不需要做出大的修改。

举个例子:
一个游戏有三个模式ModeA, ModeB, ModeC。我觉得这是一种最天然的抽象了,几乎就不用思考就可以肯定,多态一定是优于if else方式的。


使用多态的方式, 首先抽象出一个interface类似如下:

class IMode {
virtual doSomeThingA() = 0;
virtual doSomeThingB() = 0;
...
};

然后根据ModeA/ModeB/ModeC的实际玩法去实现IMode类开中函数。

这样只需要在游戏切换模式时使用如下类似代码散转一下即可:

IMode *mode;
switch (game_mode) {
case ModeA:
mode = new ModeA();
break;
...
default:
assert(!"unkonwn game mode");
break;
}

在代码主体逻辑中,仅仅使用mode指针即可,即主体逻辑并不关心当前mode是ModeA/ModeB/Modec中的哪个, 他仅仅去调用相关的IMode中的函数即可。

这样当去看主体逻辑代码时,暴露出来几乎全部是主体逻辑,而不会被模式相关的东西弄晕了头脑。

当新加一个ModeD模式时, 仅需要修改上面的Switch语句加一个case另外再实现一个class ModeD类即可, 并不需要修改之前运行良好的主体代码。也就符合了open-close原则。


再看if else的实现,假设如上IMode函数有50个函数DoSomething1~50, 如果使用if else的方式实现,则需要在调用DoSomething1~50的地方全写上类似如上switch的语句,需要搞清楚的是IMode有50个函数并不意味着这50个函数仅仅调用50次(甚至说是150次都不算多), 那么就会出现代码量暴增。

而且这样会导至所有的模式代码是以函数分开的,在增加每一个模式时主体代码都要增加50个函数并修改150处switch-case语句,如果改漏一处就会出现难以预料的bug, 这样大大增加了代码的维护难度。

当然这种还算是好的情况,如果在IMode类中的函数非常短,那么在使用if else方式实现时有些人偷懒就可能会直接用case一段代码语句直接搞定,甚至在后续开发中都可能出现这样的代码:

switch (game_mode) {
case ModeA:
case ModeB:
do_something();
if (game_mode == ModeA)
do_sometingA();
else
do_somethingB();
break;
...
};

如果有4个模式这样纠缠到一块,我打赌你看到这段代码时一定会想知道他家在哪里。

这样修改还有一个坏处就是并不符合open-close原则,因为你在改ModeB时可能会把ModeA改坏,而且有时后会很难测出来。


通过比较很容易就可以看出,多态和open-close的好处,在这里我并不想听到if-else/switch-case比使用多态效率高这种鬼话,我认为损失一点点性能能获得这么清晰的代码结构是非常值得的。

在《Unix编程艺术》P14中说过这样一句话,计算机编程的本质就是控制复杂度,比较两种实现方式可以看出第一种方式,不论增加多了种模式其复杂度几乎都不会增加,而第二种方式每增加一种模式都会使得代码变得更糟糕一些,而且永不停止直到他再也无法被维护。

bitfield数据类型的坑

bitfield并不具有可移植性,因此实际使用中,我都是尽量使用bitand来代替。

然而代码中之前就已经使用了bitfield的定义方式,作为后续开发我没有理由去改掉这个数据结构(除非它有问题),结果就无意间踩到了这个坑。

bitfield定义和使用大概如下:

union utest {
int val;
struct stest {
int a:3;
int b:5;
};
};

union utest t;
t.value = 0x07;

bitfield冒号后面的数字标识bitfield的位宽,bitfield前面的类型用于标识取出字段后应该变成一个什么样的类型(标准上说仅能支持int, signed int, unsigned int, 然而gcc还支持char, short等类型)。

问题的关键就在于,如果你定义的是有符号类型,那么编译器会将取出的bitfield按照有符号类型进行类型提升

当程序读取变量stest::a时,他会读取utest::val的byte0的低3bit。由于stest::a的类型为int(有符号型类型), 则他将utest::byte0::bit2作为符号位进行整型提升。

也就是说如果utest::byte0::bit2~0的值为110b, 那么你读stest::a时,编译器会将bit2作为符号位来将110b整型提升为0xfffffffe, 即(int)-2;

在实际使用中我使用stest::b作为了一个数组的索引,当stest::b大于0x10时,数组访问直接越界了。

btw, 一般使用bitfield特性时应该很少去依赖于其符号扩展功能(即将其定义为有符号型类型), 因此在将bitfield定义为int而不是unsigned int时一定要再三考虑。