indy 10终于随着Delphi2005发布了,不过indy套件在我的印象中总是复杂并且BUG不断,说实话,不是看在他一整套组件的面子上,我还是喜欢VCL原生的Socket组件,简洁,清晰。Indy9发展到了indy10几乎完全不兼容,可叹啊。言归正传。在使用IdTCPServer组件的时候发现了他的漏洞,他的OnConnec,OnExecute,OnDisconnect等事件是在其他线程中执行的,通常情况下这没有问题,但是在特殊的情况下会造成问题,如果其他部分的程序写得有问题就会出现漏洞。
我发现的漏洞是这样的,我在OnDisconnect事件中释放一个ListView的一个对应的Item,也就是,一个客户端离开的时候,界面上的ListView对应的项目删除。正常情况没有任何问题,但是,如果不断开连接直接关闭程序就会死掉,事实上我在程序中Form的CloseQuery事件中作了处理,在这个事件中我关闭连接,但是,没有效果,只有在程序中手动的点鼠标关闭才不会死掉,问题出在哪里?
问题出在这里,ListView是一个Windows的标准组件,释放他的一个Item是通过消息完成的,也就是说,我在OnDisconnect中在一个非主线程的线程中向主线程的窗口发送消息并且等待返回。而此时主线程在干嘛呢?因为是主线程触发的DisConnect,所以他在等待这个端口的服务线程挂起,这样就发生死锁,问题在于,主线程的等待服务线程挂起的处理代码不当,它理论上应该在等待同时处理消息。原代码如下所示
procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
LYarn: TIdYarnOfThread;
begin
LYarn := TIdYarnOfThread(AYarn);
if LYarn.Thread.Suspended then begin //判断是否挂起了,挂起了才释放线程
// If suspended, was created but never started
// ie waiting on connection accept
LYarn.Thread.Free;
FreeAndNil(LYarn);
end else begin
// Is already running and will free itself
LYarn.Thread.Stop; //没完没了的调用Stop过程,却不处理任何消息和同步事件
// Dont free the yarn. The thread frees it (IdThread.pas)
end;
end;
它的上一级调用者,没完没了的判断服务线程数量,然后没完没了地调用上面这个函数,调用者原代码如下
procedure TIdTCPServer.TerminateAllThreads;
var
i: Integer;
begin
// TODO: reimplement support for TerminateWaitTimeout
//BGO: find out why TerminateAllThreads is sometimes called multiple times
//Kudzu: Its because of notifications. It calls shutdown when the Scheduler is
// set to nil and then again on destroy.
if Contexts <> nil then begin
with Contexts.LockList do try
for i := 0 to Count - 1 do begin
// Dont call disconnect with true. Otheriwse it frees the IOHandler and the thread
// is still running which often causes AVs and other.
TIdContext(Items[i]).Connection.Disconnect(False);
end;
finally Contexts.UnLockList; end;
end;
// Scheduler may be nil during destroy which calls TerminateAllThreads
// This happens with explicit schedulers
if Scheduler <> nil then begin
Scheduler.TerminateAllYarns;
end;
end;
说实话,我很不理解indy线程对象又是stop又是start的复杂模型意义何在,而且非常容易出问题,简单的线程模型更加可靠和实用。
修改的方法很简单,但是考虑到兼容Linux和其他平台的问题,还必须进行隔离分解层次,所以稍微复杂了一点点。就是在idThread类中增加一个公开的方法ProcessMessages,然后在TerminateYarn中调用。代码如下
procedure TIdThread.ProcessMessages;
begin
{$IFDEF MSWINDOWS}
if GetCurrentThreadID = MainThreadID then
begin
CheckSynchronize;
Application.ProcessMessages;
end;
{$ENDIF}
{$IFDEF LINUX}
if GetCurrentThreadID = MainThreadID then
begin
CheckSynchronize(1000);
end;
{$ENDIF}
end;
procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
LYarn: TIdYarnOfThread;
begin
LYarn := TIdYarnOfThread(AYarn);
if LYarn.Thread.Suspended then begin
// If suspended, was created but never started
// ie waiting on connection accept
LYarn.Thread.Free;
FreeAndNil(LYarn);
end else begin
// Is already running and will free itself
LYarn.Thread.Stop;
LYarn.Thread.ProcessMessages; //此处增加了处理。
// Dont free the yarn. The thread frees it (IdThread.pas)
end;
end;
TidThread所在单元增加uses
{$IFDEF MSWINDOWS}
Windows, Forms,
{$ENDIF}
至此程序正常。
虽然理论上,在其他线程中访问VCL组件应当使用线程同步方法,但是在这个例子中由于工作线程包装太深已经无法使用线程同步方法了,其实在这个例子中即使使用了线程同步方法来访问VCL也无济于事,因为在等待线程结束的程序中根本没有检查主线程的未结消息和线程同步事件(Delphi7以后版本线程同步方法使用事件event作为同步方法,Delphi7之前使用消息同步)一样会死锁。
其实VCL并没有强制主线程访问,他的说明是这样的,它要求在任何时候应当只有一个线程访问VCL的相关代码(不同的线程同时访问不相关的VCL代码段是没有问题的),所以,不使用线程同步方法访问VCL也是可以的,但是要使用其他方法保证访问的唯一,比如可以使用临界区。
下面是使用SSL的心得。
开源给我的印象真的是越来越糟糕,早些时候处理MySQL的版本兼容性就觉得受不了,文档不全,版本差异显著,不同版本的外部接口不一致,甚至都不知道要保持向下兼容性,范例少,没事就是叫你去看源程序,说实话大多数时候做开发哪有那么多时间去人家的源程序啊,一会儿pascal,一会儿C++,一会儿在Pascal中不打;结尾,一会儿在C++写起了begin end,累啊。
indy中的SSL组件是IdServerIOHandlerSSLOpenSSL和IdSSLIOHandlerSocketOpenSSL,前一个用在服务器,后一个用在客户端。
刚开始用,找不到DLL,我想大家都遇到,看了半天E文,哦,他用OpenSSL的,哪个是什么玩艺?下载一个吧,原来是开源项目,还是多平台的,我日,要我安装perl生成mack文件,还要安装VC6才能编译成二进制代码,晕,我只要2个DLL,不至于这么折腾我吧。算了,再去找,哦?有编译好的下载,不错。有个最新版的好像是0.9.7c不过比我那个源代码的0.9.8版本低了一点,算了不管了,下载完毕,拷贝DLL过去,我日,版本不对,DLL外部接口函数不正确。indy究竟用哪个版本的呢?在indy官网上看了半天他也没说他的indy10基于哪个版本的,可见这种组件供应商的素质。没辙,下载了一Demo,Demo中包含了这两个DLL,好,拷贝过去,能够用,总算开工了。
开工了,它不工作,看帮助,indy的帮助啥也没有,有的简陋到居然只有一句话,这是个string类型的属性,这要她说吗?没辙,只好看Demo去(我现在有点理解开源的含义了),这么多年养成的好习惯都费了,以前做开发,用到新组件,首先就是看官方帮助,微软的那个帮助叫做详细啊,完了,现在第一件事情是去看Demo,还不知道是不是标准的范例,不知道是不是能够涵盖所有的内容。看了半天范例,原来要证书文件,我也够蠢的,SSL没证书怎么玩?证书?怎么办?看了半天,我应该需要一个自签名的根证书,然后需要一个二级证书,最后是key文件。其实我只需要一个自签名的证书就可以了。怎么搞?windows下没一个工具可以产生新证书的,IIS有,但是证书文件不知道给他藏在什么地方了。只好用openSSL生成新证书。
搞了一整天,终于看明白了OpenSLL的关键指令,大致结构有个模糊的印象,先产生一个key文件,然后用这个Key文件产生一个自签名的证书,这个证书可以作为根证书使用,更多的方法就不知道了,key文件要用des加密,用其他的加密,indy不能识别。crt要用x509协议。具体的openSSL指令如下,十多年没用DOS界面了,看样子要练练指法了。
产生一个key,当然要先启动openSSL,一个Dos界面,专业点吧,命令行界面,或者叫做XXXXXX
openssl>genrsa -des3 -out -voiceService.key 1024
下面是OpenSSL的反应
Loading 'screen' into random state - done
Generating RSA private key, 1024 bit long modulus
............................................................................++++++
........++++++
e is 65537 (0x10001)
Enter pass phrase for -ca.key:123456
Verifying - Enter pass phrase for -ca.key:123456
123456是你输入的密码,高版本的OpenSSL不会显示的。
产生自签名证书
req -new -x509 -days 3650 -key voiceService.key -out voiceService.crt -config openssl.cnf
注意openssl.cnf文件是配置文件,可以自己编辑用edit编辑,不要用记事本。不过一般用自带的,比较混蛋的是你下载的编译好的openssl里面没这个文件,只有下载源代码的才有,拷贝到openssl一起的目录,或者你喜欢的其他目录。
openssl的反应
Enter pass phrase for ca.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:zj
Locality Name (eg, city) []:hz
Organization Name (eg, company) [Internet Widgits Pty Ltd]:voiceService
Organizational Unit Name (eg, section) []:voiceService
Common Name (eg, YOUR name) []:voiceService
Email Address []:bluetrees@yeah.net
很多都是你填写的内容,记住中国的国别缩写是cn,我查了半天,我够苯啊,省份和城市缩写随便写,下面的也是随便写。
弄好以后产生2个文件
VoiceService.key和VoiceService.crt
VoiceService.crt这个文件复制一份修改后缀为VoiceService.pem,其实不修改也没事。
然后拷贝到服务器程序所在目录,在indy中加载的程序段,我这里在Form的Create事件中修改
var
appDir: string;
begin
Users:=TUsers.Create;
appDir:= extractFilePath(application.exename);
IdServerIOHandlerSSLOpenSSL1.SSLOptions.KeyFile:= appDir + 'VoiceService.key';
IdServerIOHandlerSSLOpenSSL1.SSLOptions.CertFile:= appDir + 'VoiceService.crt';
IdServerIOHandlerSSLOpenSSL1.SSLOptions.RootCertFile:= appDir + 'VoiceService.pem';
这些文件路径还不能用相对路径,只能用绝对路径,够操蛋吧。
在IdServerIOHandlerSSLOpenSSL的OnGetPassword事件中填写密码
Password:=123456;
然后就可以工作了。
对开源的感觉,在开源社区中最容易找到当老大的感觉,因为没有官方文档,没有范例,有的只是对源代码的熟悉程度。
从工业化的角度来说,完备的文档和成熟的范例才是技术进步的基石,而不是源代码。
开源社区有英雄,但是没有工程师,那里有思想,却没有规范。
商业软件看样子不可能消失,同样,开源社区也不能没有。