做了一个twisted的python项目,有点体会,不知道是否有人感兴趣

做了一个twisted的python项目,有点体会,不知道是否有人感兴趣

想写一下,不过可能会比较长,要花很多时间来写,如果很多人感兴趣我就来写一下。
项目包括了:
wxpython + twisted(tcp) + boost.python + pyaudio + pymysql(使用twisted的util接口)
期待楼主分享实战经验

好吧,那我开始准备准备,可能写的比较慢,大家见谅


QUOTE:
原帖由 chrisyan 于 2009-1-21 13:23 发表
好吧,那我开始准备准备,可能写的比较慢,大家见谅

没事哦,过年时间长着呢。呵呵。
首先介绍一下整个程序的概况:
这个是一个C/S结构的程序,Client端运行在windows上,Server端运行在gentoo linux上。windows的客户端是用wxpython写的,带有UI的界面。Server端是一个daemon进程,因为要进行认证等操作,还使用了mysql的数据库。程序的使用方式是这样的:
打开客户端,登录或者注册;通过认证以后进入程序的主界面。然后向server请求一些信息(Text文本),server再返回一些信息(Text文本)。然后开始录音,把录音后的数据经过一个用boost.python包裹的模块进行处理,然后传输出去(binary二进制数据),传输可能进行很多次,直到用户说完话,点击另外一个按钮停止传输。传输完成以后,server把处理的结果再发送回来(server在接受client的二进制数据的时候,也使用了一个boost.python包裹的模块进行处理)。过程很普通,但是涉及的东西还是很多的,而且这个过程如果真的走一遍的话,那么以后写基于tcp的twisted程序都没啥问题的了。

先讲一下boost.python库使用的过程中要注意的问题,具体的boost.python如何使用,在官方的网站上的教程已经讲了很多了,就不具体详述了,这里主要讲一些我所觉得重要的东西。

1.编译环境的设置:
  windows环境:
  首先需要下载boost库,boost.python就包含在boost库中。然后编译bjam,bjam可以看成是boost库的编译器,然后用bjam去编译boost库,最后得到一些library(一般使用dll,运行时库),这些library和boost的头文件就是我们在编译我们自己的工程所用到的,当然运行时库在运行程序的时候也是不可缺少的。网上有很多文章写怎么设置,我摘录了一些,然后经过自己的验证是可行的,在做的时候稍微整理了一下,就直接贴到这里了,我就不做细致的整理的。还有一个需要注意的地方,必须先安装好python环境,python安装包自带了一些library和头文件,这些都是编译程序时必需的。我安装的是python2.5,安装路径是c:\Python25。生成的boost.python库放在了c:\Boost\boost-1_36下面,在设置vs2005时可以看到需要这两个路径。

*********华丽的分割线**************
Boost python在VC下的使用
一.编译
1)准备工作
0.下载boost源代码包
   http://www.boost.org/users/download/

编译boost库之前,需要做一些准备工作。下载一些Open Source的包,来支持boost特定库的需要。
1. ICU
ICU提供了unicode和国际化支持,目前的版本是3.8.1。ICU的主页是http://www.icu-project.org/。
(1). 下载
可以从http://www.icu-project.org/download/3.8.html下载源代码版本和使用VS2005编译的版本。推荐下载源代码版本自己进行编译,以避免依赖于VS2005的运行时库。

(2). 编译
ICU的编译比较简单,打开ICU源代码目录下的source\allinone\allinone.sln,需要转换到VS2008格式,直接转换即可。然后,选择release,Rebuild Solution即可。

(3). 测试
将编译出来的bin目录加入到系统的PATH目录中去。然后,重新打开allinone.sln工程。

需要通过测试的项目

   1. cintltst项目
   2. intltest项目
   3. iotest

分别设置成启动项目,运行即可。

2. bzip
bzip的主页是 http://www.bzip.org/,从http://www.bzip.org/downloads.html下面下载源代码包即可,boost直接使用源代码来进行编译。

3. zlib
zlib的主页是http://www.zlib.net/,从该网页下面下载源代码包即可,boost直接使用源代码来进行编译。

4. python
python的主页是http://www.python.org/,下载python的2.5.2版本,安装即可。boost默认是会编译python,并且会自动寻找python的安装目录。


到开始菜单的VS2005菜单项下,启动Visual Studio 2005 Command Prompt,以下编译步骤均假定直接在该工具下进行编译。
1. 编译jam
到tools\jam目录下面运行build_dist.bat,编译好的放在tools\jam\stage\boost-jam-3.1.16-1-ntx86目录下,将bjam.exe复制到boost的根目录。

2. 编译boost
可以使用以下命令来分别编译dll版本和lib版本。下面是一个示例脚本的例子,其中的目录需要替换:
REM used with iostream library
REM boost_1_35_0\libs\iostreams\doc\installation.html

set BZIP2_SOURCE="E:\library\bzip2-1.0.4"
set ZLIB_SOURCE="E:\library\zlib123"

REM used with regex library with unicode support
set ICU_PATH="E:\library\icu"

REM DLL版本
bjam --toolset=msvc --stagedir=./lib_x86 --builddir=./ address-model=32 link=shared runtime-link=shared threading=multi stage debug release

REM lib版本
bjam --toolset=msvc --stagedir=./lib_x86 --builddir=./ address-model=32 link=static runtime-link=shared threading=multi stage debug release

编译好的文件放置在boost根目录的lib_x86\lib目录下,在boost根目录下的bin.v2目录是中间文件,编译后删除即可。
如果需要去掉编译过程中的一些warning,可以在tools\build\v2的user-config.jam文件中加入以下这一行:
using msvc : : : <compileflags>/wd4819 <compileflags>/D_CRT_SECURE_NO_DEPRECATE <compileflags>/D_SCL_SECURE_NO_DEPRECATE <compileflags>/D_SECURE_SCL=0 ;


二.设置
1)环境设置(全局变量,仅设置一次)
添加头文件,库文件目录
(Menu)Tools-->Options-->Projects and Solutions-->VC++ Directories-->|
                                                                                                        |Include files
                                                                                                                 |C:\Boost\C:\Boost\boost-1_36
                                                                                                                 |C:\Python25\include
                                                                                                        |Library files
                                                                                                                 |C:\Boost
                                                                                                                 |C:\Python25\libs

2)工程设置:
  a.创建一个默认MFC Dll项目;
  b.清除所有默认创建的源文件和资源文件;
  c.修改工程属性:
        C/C++
         |Code Generation--> Runtime Library  Multi-thread DLL (/MD) and (/MDd) for debug version
         |Precompiled Headers-->Create/Use Precompiled Header    No Using Precompiled Headers
         |Code Generation -->Enable Minimal Rebuild   No
        Link
         |General-->OutputFile  ?????.pyd
         |Input-->Module Definition File  设置为空

*********华丽的分割线**************
注意,本来dll工程生成的文件是xxx.dll,我已经改成xxx.pyd了,编译完成后,把这个pyd文件放在和脚本相同的目录中就能使用了。
我在使用的过程中并没有编译ICU,bzip 和 zlib,我的项目用不到,就直接略过了。
如果你要编译boost,只是看我的说明还是不够的,还是要去官网上了解一下概念。

   linux环境:
   好吧,这个是我的项目中遇到的第一个变态问题。我要包裹的模块的源代码树是用automake/autoconf来构建的,整个源代码有80M以上,而且里面的文件目录什么的关系比较复杂,并且我所要包装的功能只是需要整个源代码的一小部分,所以提取一些需要的源代码文件再通过bjam来编译就变成了mission impossible了,所以一个可行的方案是修改autotools代码树的配置,让它来完成bjam的功能。
   其实说起来也容易,首先大家要了解bjam这个东西,你把这个东西看成一个编译器,但是它其实是一个编译器代理而已,就是说它只提供一个统一的编译命令行界面,具体编译的工作还是由平台自己的编译器来完成的,在 windows平台它使用ms的编译器,在linux平台他使用gcc/g++等编译器,我们只要获得了它在linux平台的相关编译参数再加入到autotool工程中就可以了。那么如何来获得bjam的编译参数呢? 在查过bjam的使用说明后发现了一个参数:
  * -ox; Output the used build commands to file 'x'.
  Ok,这个东西就是我们想要的。开始做准备工作,下载boost源代码,编译bjam,编译boost.python.当这些都完成了以后,把那个boost.python的hello world拿过来,写好Jamroot的配置文件,编译通过。然后加上 -ofile 再来执行一下,生成了一个名字叫file的文件,里面就有我们需要的编译参数。
  经过分析我们需要加入的参数是 -Wl,-Bstatic -Wl,-Bdynamic -ftemplate-depth-128 -finline-functions -Wno-inline -Wall -fPIC -pthread -DNDEBUG -Wl,--strip-all -lpthread  -lutil  -ldl -lrt -L"/usr/local/boost/lib/" -lboost_python-gcc41-mt-1_36
  我靠,还真是好多都不认识,也不管了,能用就行了(态度有问题,大家别学我)。
  接下来改autotools源码树
  1.在项目树的根上建立一个pymodule的目录,和src是平行的,我不想我写的东西混进原来的代码中。
  2.在configure.in中的AC_OUTPUT里加上pymodule/Makefile
  3.编辑pymodule/Makefile.am,差不多像这样:

*********华丽的分割线**************
lib_LTLIBRARIES=libsomething.la

libsomething_la_SOURCES=something_server.cpp something_wrapper.cpp
libsomething_la_LDFLAGS= -Wl,-Bstatic -Wl,-Bdynamic -ftemplate-depth-128 -finline-functions -Wno-inline -Wall -fPIC -pthread -DNDEBUG -Wl,--strip-all
libsomething_la_LIBADD = $(top_builddir)/src/libxxx/libxxx.la -lm -lpthread  -lutil  -ldl -lrt -L"/usr/local/boost/lib/" -lboost_python-gcc41-mt-1_36

INCLUDES = -I$(top_srcdir)/include \
        -I$(top_builddir)/include  \
    -I"/usr/local/boost/include/boost-1_36" \
    -I"/usr/include/python2.5"

*********华丽的分割线**************
   libxxx.la 是项目树编译出来的我要包裹的库,something_server.cpp是我的c++包裹文件,因为原来的库是c写的,我要先包成类。 something_wrapper.cpp就是包含了boost.python的代码了,用来包裹成python模块。如果要用什么别的库,头文件,就直接修改这个Makefile.am就好了。


编写boost.python模块需要注意的地方:
首先声明,我没有花很多的时间来研究boost.python,毕竟这个只是整个项目的一小部分,只要看完了我所需要的部分就可以了。而且有很多人写了boost.python的使用笔记,配合官方的文档,基本上没有什么问题了。以下所提到的只是我对编写boost.python的一点体会,可能不完全正确,也请高手来帮我斧正。

第一点是项目文件的安排,我喜欢把boost.python的包裹部分和原有的的程序代码部分完全分开,像c的工程,我就先给我需要的部分封装了一个类(对于复杂的应用,可能是一组类),即便是c++的工程,可能也需要组织一下,用现有的类编写出一个完全和python模块对应的一个c++类,这样对于编写包裹部分就很容易的了。尤其是在包裹一个非自己的代码包的情况下,只要接口不变,你所写的包裹不必跟着所包裹的程序的升级而进行修改。对于自己所编写的c++的那个封装文件,不必引入任何的boost.python的头文件和链接库,而且比较方便的是你可以对这个封装类加一个main函数,编译成执行文件进行测试(可以使用gdb调试),测试的时候不会有boost.python任何部分的参与,等你完全测试好以后再进行包裹,那样再出问题就肯定是boost.python部分的问题了(我现在还没有找到怎么调试编译好的python模块的好方法,只有print了)。

第二点就是在写包裹的时候,我们所暴露出去的无非就是类和函数,这个在boost.python的教程里写的很清楚了(其实也没有想象中的那么清楚,有时候还是要看看源代码)。我要提醒大家注意的是类和函数所需要的参数是python的数据结构,也就是说要使用boost.python里面提供的类(因为我们是在c++里面写,肯定是用c++的库,python里基本的数据结构在boost.python库里都是有对应的),而在传给底层的c++类的时候再做一次转换。比如一下的一个包裹:
//要包裹的c++函数是 int module_init(const std::string& cfg)
//以下是包裹
void init_module(boost::python::str cfg)
{
    std::string config = extract<std::string>(cfg);
    module_init(config);
}

再如下面,返回一个tuple
//要包裹的c++函数是 void module_process(const std::string& trans,const std::vector<float>& flvec,std::vector<ScoreItem>& phone,std::vector<ScoreItem>& word);
//包裹是
boost::python::tuple process(boost::python::str trans,boost::python::list py_data)
{
    std::string transcription = extract<std::string>(trans);
    boost::python::stl_input_iterator<float> begin(py_data), end;
    std::vector<float> data(begin,end);
    std::vector<ScoreItem> phone;
    std::vector<ScoreItem> word;
    module_process(transcription,data,phone,word);
    return make_tuple(phone,word);
}
大家注意上面的函数了吗?虽然返回的是一个tuple,但是你只是这么写在python解释器里是没什么用处的,你只得到了tuple,但是没有暴露tuple里面所包含的对象,你无法调用任何的操作。我们注意到tuple里面的对象是std::vector<ScoreItem>,那么我们就必须把vector 和 ScoreItem(这个只是一个struct,没有定义方法,只是用来保存一个结果的集合)都暴露出去。
下面暴露vector,我们没有必要把vector的所有方法都暴露出去,是要暴露我们想要的就行了,
   class_<std::vector<ScoreItem> >("ScoreList")
        .def("__iter__",iterator<std::vector<ScoreItem> >())
        ;
这样写完以后,我们只是把vector的迭代器暴露了,这样已经足够了,有了这个,我们就可以在python脚本里面使用
for i in ScoreList对象 :
   ......
而在python脚本里,上面的这个i就是ScoreItem(这个不用解释了吧!),所以再把ScoreItem暴露出来:
    class_<ScoreItem>("ScoreItem")
        .def_readwrite("sframe",&ScoreItem::sframe)
        .def_readwrite("eframe",&ScoreItem::eframe)
        .def_readwrite("score",&ScoreItem::score)
        .def_readwrite("word",&ScoreItem::word)
        ;
有了这些,你就可以在python脚本里这样写了:
import some_module
some_module.init_module('/path/to/config')
trans = 'some transcription'
input = []
...... #初始化list
result = some_module.process(trans,input)

for i in result:
   print i.sframe,i.eframe,i.score,i.word

............

接下来再说一个东西,比如你有一个类,和一个方法,在c++中这个方法是以这个类为参数的,你想把这个类和这个方法都暴露出去。那么在暴露的时候你可以做的更好一些,你可以在暴露的时候把这个方法做的像这个类的成员函数一样:
//要暴露的类为DecServer
//要暴露的方法为std::vector<short> preprocess(std::vector<float> data)
先写一个函数:
void new_decode(DecServer& d,const boost::python::list& py_float_list)
{
  boost::python::stl_input_iterator<float> begin(py_float_list), end;
  std::vector<float> tmp_samples(begin,end);
  d.Decode(preprocess(tmp_samples))
}
然后暴露类,使用了新的方法,而不是原有类的方法:
  class_<DecServer>("Recognizer",init<std::string>())
    .def("StartDecode",&DecServer::StartDecode)
    .def("Decode",&new_decode)
    .def("EndDecode",&DecServer::EndDecode)
    .def("Get",&DecServer::Get);

boost.python的部分就这么多了,只是一点体会,希望大家提意见

好吧,接下来还是先讲一讲twisted的部分吧,由于做的项目是公司的,我不好直接贴代码上来,我会照着样子简化重写一下,这个可能就比较费时了,大家别嫌慢。
写基于tcp协议的网络连接程序比起udp的程序还是容易了很多,不用自己来考虑数据包的顺序,验证数据包的正确性(这个还没有仔细的研究过,对于重要的应用可能要在协议中加入数据的验证),重传等问题。对于一个tcp的应用来说,我们需要定义一个或使用现有的一个应用层的协议(tcp属于运输层,是我们所使用的应用层协议的下一层协议).现有的应用层协议有很多,http,smtp等都是应用层的协议。对于我写的这个程序,我自定义了一个简单的应用层协议。大家知道协议就是client和server之间的通信协定,比如client发了一个信息给server,这条信息包含了client的一个请求并且可能包含了一些数据,server则要根据现有的情况(状态)和client的请求做出回答(可能包含数据)比如下面的:
client                         server
准备解码------------------------>
<-----------------------------准备好了
数据 5632 xxxxxxxx ------------------>
<-----------------------------收到
数据 2345 xxxxxxxx ------------------>
<-----------------------------收到
.
.
.
.
结果给我------------------------>
<-----------------------------结果如下
没啥事了------------------------>
<--------------------------那以后别联系了
链接关闭

一个简单的协议就像这样的。这就引入了两个问题,一个是传输数据的格式,另外就是状态的控制。
先讲状态,一般协议都会设计一个状态机,根据原有的状态和接收到的数据进行处理和状态的跳转。比如上面的,client发了一个"准备解码"给server,server就准备好了解码器,server现在的状态就是ready for decoding的状态,它可能接收到的请求一个就是发送过来的数据,另外就是请求结果,接收到了请求就会有一些反馈给client(也可能没有),状态就会做相应的跳转。如果接受到的请求时发送过来的数据,那么处理数据,状态保持不变;如果接受到的请求是要求结果,那么就返回结果,并且把状态从解码的状态切换到一个其他的状态,如果这时候再发数据过来,server就没法处理了,会出错。

另外一个就是数据的格式,一种是text的格式,发过来的全是ascii的字符,这样就很容易解析,当在必须要传输二进制数据的时候,可以使用base64编码把二进制数据全部映射到ascii字符上,在另一端再进行解码。而且在传输的时候要定义一个分割符,因为tcp是流,数据会源源不断的过来,你必须知道什么时候是一个请求的开始,什么时候是一个请求的结束,在一个请求之中哪些是命令部分,哪些是数据部分。一般我们用\r\n来分割行(也就是请求),在一个请求内部如何分割不同的部分就自己定义了。比如smtp会使用空行和一个随机的字符串来分割。另一种就是像我写的,把二进制的长度写进请求,在读到长度后,顺次再读长度所标识的字节的数字,读完以后,二进制数据的部分也就结束了。相比较来说我比较喜欢第二种,优点是:1.不需要进行编码,解码。大家知道base64编码以后数据会变大,而且编解码也需要时间,需要额外的代码进行处理。2.在读数据的时候不需要时刻检查。利用特定的分隔符做结束方式的,在传输的过程中要时刻检查标志数据结束的标志有没有出现,而且对于正常数据中出现了的结束标志符要进行转意,防止解析错误,比较耗时,耗资源。

到这里又要被打断一下,我们要传输二进制数据,比如我要把一个float类型的数据1.234传到另外一端应该怎么传呢?我们看看python 中的socket模块的sendall函数的参数说明
     |  sendall(self, *args)
     |      sendall(data[, flags])
     |      
     |      Send a data string to the socket.  For the optional flags
     |      argument, see the Unix manual.  This calls send() repeatedly
     |      until all data is sent.  If an error occurs, it's impossible
     |      to tell how much data has been sent.
我靠,我明明要传一个float,但是参数是一个string,这可咋办啊,来再看看twisted中传输数据的方式,在protocol中有一个transport对象,传输数据的时候调用transport.write()方法,参数也是string.就是说我们一定要使用string了,那就把float转成string吧,这个操作大家都比较熟悉,str()函数强转啊!
>>> str(1.234)
'1.234'
这样就是一个string了,不过我的float如果是1.23345456676767呢?
>>> str(1.2334545667676)
'1.23345456677'
我靠,还真长的一个string啊,最不爽的是,每个float的长度是不一样的,那我发过去另外一端怎么来知道哪几个字节是属于哪一个float呢?
这样不是一个正确的方法,正确的方法是使用struct模块
>>> import struct
>>> struct.pack('!f',1.234)
'?\x9d\xf3\xb6'
>>> a = struct.pack('!f',1.234)
>>> a
'?\x9d\xf3\xb6'
>>> struct.unpack('!f',a)
(1.2339999675750732,)
>>> len(a)
4
大家看到了,struct用pack来把其他类型的数据变成string,使用unpack把string类型的数据再变回来。而且对于相同类型的数据,生成的string是定长的。
struct.pack('!f',1.234)
其中第一个参数'!f'中的!意思是使用网络字节序(这个就不再细讲了,大家google一下big endian ,little endian),后面的一个f代表了把第二个参数编程float类型。
    pack(fmt, *args)
        Return string containing values v1, v2, ... packed according to fmt.
我们看到,pack可以接受很多的参数的,可以一起把很多的数据变成一个string,比如我们要一起变三个float
>>> struct.pack('!fff',1.234,2.345,3.456)  
'?\x9d\xf3\xb6@\x16\x14{@]/\x1b'
那么要在第一个参数中加入额外的两个ff,一共三个f表示变三个float类型。注意f标志(如果是其他类型就用相应的符号,具体查struct的文档)的数目要和后面变量的数目相同。
如果我有一个list保存了很多float值
data = [1.232,454.234,454.123,565.321,1.9]
我要一起都变成string,那么这样做就好了
>>> data = [1.232,454.234,454.123,565.321,1.9]
>>> struct.pack('!'+'f'*len(data),*data)      
'?\x9d\xb2-C\xe3\x1d\xf4C\xe3\x0f\xbeD\rT\x8b?\xf333'
>>> a= struct.pack('!'+'f'*len(data),*data)
>>> a
'?\x9d\xb2-C\xe3\x1d\xf4C\xe3\x0f\xbeD\rT\x8b?\xf333'
>>> len(a)
20

待续..

期待楼主分享实战经验
支持
顶, 正在写一个twisted的程序, 期待楼主写完。
期待LZ分享
更新时间
Jan 21,2009 更新
Jan 22,2009 更新
Feb 2 ,2009 更新一小段