silly的socket模块重构

最初, 我仅仅最只想将silly实现成一个socket异步框架, 每一个socket有数据或事件过来直接将注册的处理函数异步回调即可。

然后, 随着三国杀的一步步实现, 我发现我之前考虑处理时漏掉了数据库环节。

由于所有socket事件均为异步, 当一个client发过来一个请求, 而这个请求需要使用到数据库数据时, 可能就会写出类似下面这样的代码:

socket.recv(fd,function(fd, data)
--process segment1
db.get(key, function(value)
--process segment2
end)
end)

这种异步代码写起来非常费劲, 而且如果通信协议设计不好, 当–process segment2还没有被执行, 下一条socket的数据包又过来, 就有可能造成错误的结果, 而且不容易发现问题所在。

这次的重构,我希望可以在上层屏蔽掉底层的异步逻辑,这样代码写起来会更清晰不容易出错。


大概设计是这样的。

每一个socket在建立链接之后, 便拥有一个coroutine, 和一个数据队列。
每一个socket数据包过来之后均使用这个socket的coroutine来处理,如果coroutine在处理数据包过程中挂起,则将数据推入此socket的数据队列中.
每个socket的coroutine恢复并处理完上一个数据包后,会检查队列是否为空, 如果队列为空则yield等待下一下数据包的到来, 否则将消耗完数据队列中的数据。

由于socket/tcp具有fifo的特性, 如果目标数据库服务器的request/response同样是按照fifo方式处理的话, 就可以实现一个socketfifo模块(如果是其他方式,只要对sockefifo稍加修改即可)。

socketfifo模块用来在每一个socket的coroutine中管理数据库socket的命令/应答,在向数据库socket发送命令后阻塞此socket的coroutine, 直到获取到数据库socket发过来的数据。

socketfifo持有一个coroutine队列。

socketfifo向数据库socket发送命令之后, 将当前的coroutine(即调用socketfifo模块的socket所在的coroutine)塞入coroutine队列中, 然后挂起当前的coroutine。
在socketfifo收到数据后,按照fifo的特性,从coroutine队列中取出一个coroutine将其唤醒并将数据返回给此coroutine。这样即可达到阻塞访问socket的目的。

虽然用同样的方法实现了阻塞connect, 但如果connect函数并非是在coroutine中调用就需要手动创建一个coroutine来将connect函数包住。


在重构过程中发送一个关于客户端主动close事件的bug。

抽象上看, 整个silly是通过两个通道来运行起来的。

socket –> worker (silly_queue) 主要向worker通知客户端的行为事件
worker –> socket(pipe) 主要向socket发送操作命令

最初设计时, 当客户端断开连接时, socket模块随即就释放所有资源,但是此时有可能silly_queue中还有此socket的数据包。
而socket模块中是有链接池存在的, 当某个socket被释放回链接池之后, 是有可能被分配给其他client的。

假设有两个socket, s1, s2。
在极端的情况下, 当s1主动断开链接时, 恰好worker还有s1的数据没有处理完,因此worker还不知道s1已经断开了,但此时s1所占用的struct conn结构体已经被释放到链接池了。
此时s2连接到服务器,恰好将struct conn分配给s2使用。

由于silly分配的socket id的特殊性,虽然s1和s1拥有不同的socket id, 但是他们都能索引到同一个struct conn结构体,如果此时处理s1数据包的函数需要向s1发送数据, 此时其实是向s2发送的数据包, 就会产生数据错乱的情况。

因此修改了一下socket关闭的策略, 当检测到client关闭socket之后,释放除struct conn结构体之后的所有资源,然后将struct conn置成一个特殊状态,直到worker收到client的关闭消息后再调用close函数,将struct conn归还给链接池。

在worker在主动关闭socket时,同样存在另外一个队列缓存问题, 当worker向socket模块发送close命令时, 并不知道silly_queue中是否还有此socket的数据,那么socket.lua模块就不知道应该什么时间释放与此socket有关的资源, 过早的释放资源容易引起其他问题。

因此在worker向socket模块发送shutdown(worker主动关闭socket命令定义为shutdown, 与tcp的shutdown无关)之后, socket模块释放除struct conn结构体之外的所有资源, 其后将struct conn置成一特殊状态,向worker回一条SILLY_SOCKET_SHUTDOWN消息, 当worker处理SILLY_SOCKET_SHUTDOWN消息时, 肯定已经消耗掉silly_queue中的与此socket有关的数据了。

此时socket.lua可以释放与此socket有关的资源,并调用close命令将struct conn结构体归还给链接池。

btw, 将多进程改进为多线程时,我曾以为可以躲避通信的复杂度, 现在看来, 当时想的太少了。 只要按照多进程的设计思路(使用队列通信), 不管是否真的是多进程, 异步通信协议的处理是必不可少的。



发表评论