silence

silence依旧是我新开的坑,不过目前只是勉强能跑的状态,就我自己知道的,bug挺多的……不过这个东西本身只是个试验品性质的玩具,主要用途其实是用来熟悉curses编程和套接字编程。虽然成品和我计划的相比简陋了不少,大致的意思还是明白的……嘛。

silence的源代码我放在github上了,有兴趣请戳: silence

这个玩具写作时间很长,唯一的原因就是自己正式工作了,工作来说基本比较辛苦,所以也请不要在意更新的频度才是呢。总之至少我会保证这地方不会荒废到长草的(笑)。

另外说一下,严格来说,站在我现在的角度,这个项目是不合格的,当然以前的项目也是。

最大的问题在于没有合理的异常捕捉和日志的打印,而且有炫技倾向。虽然就这种玩具来说这些都不是严重的问题,但是如果是在真正的项目里面,会弄得很麻烦的……前一段时间我就被我“漂亮到不需要异常捕捉”的代码坑得不行……orz。

silence其实由一个很简单的原型演变过来的,原来的简单模型请看这篇文章

其实上面这个就可以工作得很不错了,不过后来我想用curses写个界面……话说我本来想得很简单的。

所以先来讨论界面这部分。

silence项目里面有一个prepare文件夹,里面是什么step1,step2之类的。其实那个就是我当时的学习过程。

step1是最简单的一个文件,虽然比hello world复杂一点,但是也复杂不到哪里去。

当然step1里面使用的是curses.wrapper,这是一个python提供的快捷方式,大致来说,帮忙做了curses.initscr还有curses.cbreak什么的。事实上,这个封装我倒是并不认为好用,所以后面的代码都没有使用这个wrapper。

step1会打印两句话,一句是"hello world",在最左上;另一个是"good luck",会紧接着显示在下一行。然后程序会阻塞住等待输入字符,如果输入的是q的话,退出程序,否则打印。打印的话我必须得说说,curses有一个回显模式(echo)和非回显模式(noecho),回显模式会默认把输入的字符显示在屏幕上。虽然很多时候这种设计都很方便,但是考虑一下类似于vim的normal模式,这种情况下并不希望显示输入字符,所以可以调用非回显模式。事实上,noecho的选项才是更常用的,因为我们希望能对显示有所控制。在curses programming how to里面其实有所讨论。

cbreak是更常用的一个选项,设置了cbreak,可以避免输入被缓存。就我自己使用情况来看,我是绝对不希望自己输入被缓存的。

一个需要留心的问题是,总是应该在curses程序结束前调用endwin,否则终端的显示会乱成一锅粥。当然,如果是直接使用curses.wrapper,那么倒是不存在这个问题就是了。

另外一个问题是,如果一行的字数超过了终端大小 —- 比如说一个80x24的终端,如果一行希望显示81个字符,那么curses会直接报错退出,所以必须谨慎控制换行才是。Y轴方向同理。另外,终端大小变化的时候,如果没有处理好,也很容易异常退出 —- 毕竟curses是有一段年头的老玩具了……

step2引入了两个新东西,一个是非阻塞,另一个是重绘。step2的大意是,使用一个列表来存储所有输入的行。行的判定是一串字符串后面是否跟着enter键。每次enter键的键入,都会清空一次屏幕,然后再从左上角开始,依次重绘列表中的内容。nodelay模式的开启其实不是为了这份代码,这里只是个实验而已。nodelay模式下,stdscr.getch()会一直无阻塞地返回值,只不过,如果确实getch没取到值,会返回-1,所以,如果取到了-1,continue就好。不过可以想见,这样的方式会使得cpu占用率飙高,其实这倒不是什么大事,经常sleep就好。

step3会提示输入一个文件名,然后会在屏幕中显示该文件的末N行,其实step3很有用,因为step3有一个漂亮的分行函数,以及,step3支持utf-8。

支持utf-8比想象中要容易一些,按照官方文档的说法,首先需要这几句话:

import locale
locale.setlocale(locale.LC_ALL,'')
code = locale.getpreferredencoding()

不过当然不会这么简单,接下来需要考虑的是,怎么通过getch来读取非ascii字符。庆幸的是,getch比想象中的聪明,当键入的是非ascii字符时,getch不会报错,而是返回一串数字。稍微研究一下就知道,其实就是把类似于\xe6\x82\xa8这样的东西转成了十进制。

如果有兴趣的话,这两个小函数就工作得很好。

def combine(func):
    def wrapper(*args,**kwargs):
        value = "".join(reversed(list(func(*args,**kwargs))))
        return value
    return wrapper

@combine
def parse(value):
    while value:
        ch = value % 1000
        value /= 1000
        yield chr(ch)

step4基本就是后面写的ui.py的原型了,代码和思路都很类似,不过ui.py因为混了太多东西,所以反倒不容易看,这份会更清晰一点。

curses由于比较原始,所以定位呀这些东西都得自己来……其实很烦的,老实说。

step4里面,其实我模仿了一下vim,虽然只是稍微模仿了吧……这个ui有两种模式,一种是插入模式,一种是normal模式,虽然normal模式只有hjkl命令……通过esc键从insert模式转成normal模式,i键从normal模式转成insert模式。当然两个模式很少,所以这份代码的做法问题不大,更多的模式的话,一个更好的思路是维护一个字典,key可以是用来区分的内容,0,1,2 or insert, normal, command都可以,value是一个函数对象。稍微想想也很简单……

然后就是ui.py。说起来,其实前面的所有小片段都是为这个文件服务的……

大体上,和step4很像,然后如果记得的话,会知道step2里面实验了nodelay模式。原因是,输入源不只是用户的输入,还有对端发送过来的信息。任意一种情况的发生,都需要重绘整个屏幕,所以阻塞是一定不行的,阻塞在用户输入上,会导致用户输入提交后,出现输入内容以及对端传送的内容,总而言之是很奇怪的。

ui的append是提供给外界的接口,用来在content中增加内容的。

socket_send这个函数的出现是因为这个:在终端输入的时候,我们可以把终端输入看成一个文件描述符(本来就是),然后select可以无差别地去处理套接字描述符和终端输入。但是,有了ui后,就不能用这个方法了,只能通过新建一个套接字来收发内容。

我试过使用AF_UNIX以及AF_INET,不过这个项目里面两个都有用就是了。AF_UNIX我个人不太熟,所以用得也少,于是处理socket_send的方案只能是通过AF_INET绑定一个本地端口,然后通过这个端口收发,这部分后面说。

关于ui.py貌似就没多少好说的了,虽然看着这么简单,但是当时折腾了好久……各种奇怪的问题挺多的。另外就是耦合度,紧密得我几乎不能忍了,话说最开始想的是松耦合呢,为什么变成这样了呢……(望天。

然后就是chat_server.py和chat_client.py。

首先说一说,最开始设计的时候我考虑过解耦了,所以分开了ui和套接字的操作,这就产生了一个问题。select依赖于一个while True循环,ui里面也有一个while True循环,所以必然这里需要多线程执行。最开始我考虑过gevent,但是结论就是gevent不适合。首先,chat_server.py和ui.py本身都是非阻塞的,所以这一点就排除了gevent的可能性。不过,即使是阻塞的也不行,我之前的实验结果是,即使是阻塞的,gevent也会挂死在ui的循环里面,所以多线程在这里很适合。

另外就是,可以注意到,套接字使用得很多,除了最主要的server和client套接字之外,都有一个interrupt套接字,这个套接字是AF_UNIX的。其实AF_UNIX和AF_INET在这里都是一样的。这个套接字只负责一件事,通知chat_server线程结束。当在ui中的normal模式键入q的时候,ui本身会退出,但是没有一个机制的话,chat_server线程还是一直在运行的,所以使用这个套接字来告知chat_server中断退出。安全退出的一个好处是,至少不用担心终端显示乱掉。

上面提及了socket_send,我称这个操作为回射,引入这个操作的目的是这个:键入的内容,实际上在ui.py里面,但是如果需要send出去,必须通过chat_server或者chat_client中的套接字,虽然ipc的方式很多,总而言之我选了通过套接字的方式。

大致如此,回头想想,这东西果然很简单呢,而且这个简单的东西花了这么多时间……(掩面。

谢谢阅读,AS正在考虑下一篇写些啥,其实最近的东西可写的不少,AS最近眼界也更开阔了……嘛。

祝身体健康,鞠躬。

冬天不想出被窝的AS。

以上。