WebBroker 架构分析

摘录一篇很老的文章,老是老点,但对WebBroker说得很透彻。这是本人在网上所能收到的最好的WebBroker参考资料。快18年过去了,现在读起来仍有指导意义,不能不佩服作者的学者风范。这是作者介绍WebBroker一系列的文章之一,我把最重要的粘贴到这里。本系列的其他章节我在后文做了连接。其中的“五、Web Module部分的请求调度”,我没有找到,不知道作者写了没写。有发现朋友请留言,我再增加连接,方能不辜负作者的良苦用心。

原文出处:http://www.newsmth.net/nForum/#!article/Delphi/752

发信人: flier (小海 //爱喝可乐_), 信区: Delphi
标 题: 四、WebBroker与WebReq的分析 v0.1 - 2001.8.7
发信站: BBS 水木清华站 (Wed Aug 8 00:23:28 2001)

==============================================================
VCL架构下Web服务程序实现原理分析

四、WebBroker与WebReq的分析 v0.1 - 2001.8.7
在分析完几种不同类型的Web Application之后,让我们把Web Application
再深挖一把,看看隐藏在其背后的TWebApplication以及TWebRequestHandler
到底是如何维护Web Application与Web Module两部分之间的联系的。

从上图我们可以看到,Web Application部分的不同类型实现,都是从
TWebApplication继承出来的,这个类以及其父类TWebRequestHandler
才是VCL中为了实现不同类型Web Server程序,在Web Application这一层
做到的Server类型无关抽象的代码所在。
在D5的实现里,WebBroker单元的TWebApplication实现了所有的功能,
而从D6开始,被分离为TWebApplication和TWebRequestHandler两层,
前者负责Application相关的层面的功能,后者则负责Module相关的层面的功能。
这样一来分工更清晰,职能更明确。
让我们先来看看负责Application相关层面的功能的WebBroker单元
...
constructor TWebApplication.Create(AOwner: TComponent);
begin
WebReq.WebRequestHandlerProc := WebRequestHandler;
inherited Create(AOwner);
Classes.ApplicationHandleException := HandleException;
if IsLibrary then
begin
IsMultiThread := True;
DLLProc := @DLLExitProc;
end;
end;
...
首先是初始化两个全局变量。先看看WebRequestHandlerProc变量:
...
// from WebBroker
function WebRequestHandler: TWebRequestHandler; export;
begin
Result := Application;
end;
...
// from WebReq
var
WebRequestHandlerProc: function: TWebRequestHandler = nil;
...
// from WebReq
function WebRequestHandler: TWebRequestHandler;
begin
if Assigned(WebRequestHandlerProc) then
Result := WebRequestHandlerProc
else
Result := nil;
end;
...
代码很简单,加上很少需要直接使用WebRequestHandler函数,
因为既然我们知道他和Application等同,我想不出什么理由不直接用
Application全局变量,呵呵。Borland这样做的目的大概是以函数和指针的
方式实现一种类似于虚拟函数的效果,以便在WebReq单元一级更好地抽象吧。
再看看ApplicationHandleException变量。这个函数指针相对来说
使用的机会还是很多的,他提供了Application一级的错误处理方法的抽象。
因为无论你是以什么形式的Application进行封装,如Forms, WebBroker或
控制面板Applet之类的实现,都会提供相应的错误处理手段,而这些处理手段
的抽象,就是依赖于ApplicationHandleException函数指针。使用方式类似于
...
try
...
except
if Assigned(ApplicationHandleException) then
ApplicationHandleException(Self);
end;
...
几乎所有的Application层实现都会替换掉此函数指针为自己的处理代码,如
constructor TApplication.Create(AOwner: TComponent);
begin
...
if not Assigned(Classes.ApplicationHandleException) then
Classes.ApplicationHandleException := HandleException;
...
end;
...
procedure TAppletApplication.Run;
begin
if not Assigned(Classes.ApplicationHandleException) then
Classes.ApplicationHandleException := HandleException;
end;
...
紧接着,判断是否为DLL,如果是则替换DLLProc。这个变量就比较常用了,
...
if IsLibrary then
begin
IsMultiThread := True;
DLLProc := @DLLExitProc;
end;
...
DllProc: TDLLProc; { Called whenever DLL entry point is called
}
...
TDLLProc = procedure (Reason: Integer);
...
等同于标准Win32 DLL中WinMain中对下列几种情况的处理函数
...
const
DLL_PROCESS_DETACH = 0;
DLL_PROCESS_ATTACH = 1;
DLL_THREAD_ATTACH = 2;
DLL_THREAD_DETACH = 3;
...
如果要在DLL中完成进程、线程级构造、析构处理,则使用此函数指针,具体使用方

请自行查看MSDN和Delphi帮助文档。注意这里直接就把IsMultiThread设置为True,
此变量的讨论我们在前几节已经进行,不再多说。
前面之所以要在Dll方式时替换DllProc,是因为Application一级需要有必须执行的

析构操作调用,在DoneVCLApplication中实现
...
procedure DLLExitProc(Reason: Integer); register; export;
begin
{$IFDEF MSWINDOWS}
if Reason = DLL_PROCESS_DETACH then DoneVCLApplication;
{$ENDIF}
end;
...
procedure DoneVCLApplication; export;
begin
with Application do
begin
Destroying;
DestroyComponents;
end;
end;
...
如果不是DLL,则使用AddExitProc系统函数,将Application级析构操作函数
加入到程序Exit时自动调用的函数列表中,意义同上。
procedure TWebApplication.Run;
begin
if not IsLibrary then AddExitProc(DoneVCLApplication);
end;
至于初始化Initialize,更多的作用是提供一个抽象方法供子类继承,也调用相应

的可能存在的初始化函数。
...
procedure TWebApplication.Initialize;
begin
// This is a place holder
if InitProc <> nil then TProcedure(InitProc);
end;
...
InitProc: Pointer; { Last installed initialization procedure }
...
InitProc是一个初始化用的全局函数指针。某些单元如ComObj通过这个函数指针
完成Application一级的初始化操作。
剩下的一个CreateForm是为了向后兼容而保存的,新的TWebApplication架构
通过类工厂方式实现类似功能,我们稍后详细讨论。
procedure TWebApplication.CreateForm(InstanceClass: TComponentClass;
var Reference);
begin
// Support CreateForm for backward compatability with D3, D4, and
// D5 web modules. D6 generated web modules register a factory.
if WebModuleClass = nil then
WebModuleClass := InstanceClass
else if WebModuleClass <> InstanceClass then
raise Exception.CreateRes(@sOnlyOneDataModuleAllowed);
end;
另外,有一个声明我还没有发现到底哪里用到了他,呵呵
TServerExceptionEvent = procedure (E: Exception; wr: TWebResponse) of object
;
然后我们看看负责Module相关层面的功能的WebReq单元。
D6的WebReq单元将D5中TWebApplication类中关于Module操作部分抽象出去,
因为要实现同时支持多个WebModule一起使用,其实现相对D5来说复杂得多。
但只要你抓住其主要思路,他的实现还是非常清晰的。
关键在于两个List:Factory List和Web Module List。
让我们先来看看Factory List。
TWebModuleFactoryList的实现代码很简单,核心内容就是维护一个TObjectList
保存着一系列的TAbstractWebModuleFactory,另外有一个特殊的AppModuleFactory
...
procedure TWebModuleFactoryList.AddFactory(AFactory: TAbstractWebModuleFacto
ry);
begin
if FList.IndexOf(AFactory) <> -1 then
raise Exception.Create(sFactoryAlreadyRegistered);
if AFactory.IsAppModule then
begin
if Self.AppModuleFactory <> nil then
raise Exception.Create(sAppFactoryAlreadyRegistered);
Self.FAppModuleFactory := AFactory;
end;
FList.Add(AFactory);
end;
...
以上是Add Factory时的代码,非常简单。如果Factory已经存在则引发异常;
如果Factory是AppModule则检查是否已经存在一个AppModule,是则引发异常,
否则保存之,然后将Factory加入到列表中。
这个IsAppModule在WebFact单元中得到支持
...

...
其中TWebApp*ModuleFactory在IsAppModule中返回True,其余的返回False。
而使用到这一属性的地方只有WebCntxt单元的TWebContext.FindApplicationModule
这个方法被用于在一个ModuleList中查找AppModule,以便从AppModule中查询相关接口

我们以后介绍WebSnap时将再详细讨论。而TWebContext在D6帮助中更是直接说明为
Help for this feature or language element was unavailable
when this product shipped. Please check your release notes
(located at the root installation folder) for Help update information.
这些我们暂且不谈,以后有时间我再详细说明TWebContext这个信息提供者或者说
连接纽带的作用。
我们接下来看看Web Module List。
以前在D5中,每个Web Server程序只允许有一个WebModule,TWebApplication
在CreateForm时将传入的WebModule的class(注意是Class而不是Object,因为D5
也实现了我们马上要谈到的Module Pooling,因此必须保存Class以便在适当时候建立新

实例)保存到FWebModuleClass,然后在适当的时候,直接使用这个class建立新的实例

这和我们上面看到的D6的CreateForm代码相同,但实际上在D6中,CreateForm方法
已经失去了原来的作用,取而代之的是使用前面介绍的Factory List和即将介绍的
Web Module List,允许用户同时使用多个Web Module。D6只是为了向后兼容才保留。
D6中的Factory List的作用类似D5中的FWebModuleClass,用于在适当时候建立
Web Module实例,但因为D6支持同时使用多个Web Module,因此,在D5中将实例直接
保存的方法在D6中改为以一个TWebModuleList保存一个线程内的一系列Web Module,
然后在TWebRequestHandler中维护一个Module List Pooling,池中每个Item是一个
TWebModuleList。而需要时建立TWebModuleList实例的工作由TWebModuleFactoryList
完成。因此我前面才有两个List一说。TWebRequestHandler主要代码都是围绕着
一系列TWebModuleList的管理。让我们来看看TWebModuleList的实现。
TWebModuleList中有两个主要的List,
...
TWebModuleList = class(TAbstractWebModuleList)
...
FFactories: TWebModuleFactoryList;
FList: TComponentList;
...
FFactories如前所述,保存着TWebModuleList对应的Factory List,
FList则保存此WebModuleList中的已有Web Module Instance。
限于篇幅原因,简单的代码我就不多罗嗦了,让我们来看看几个比较重要也相对
复杂的方法。首先是向WebModuleList里面加入Web Module。
...
TWebModuleList = class(TAbstractWebModuleList)
...
function AddModuleClass(AClass: TComponentClass): TComponent; override;
function AddModuleName(const AName: string): TComponent; override;
procedure AddModule(AComponent: TComponent);
...
Add的方法有很多,最直接的莫过于AddModule,直接将一个现有的WebModule
加入到列表中,而AddModuleName和AddModuleClass则分别是通过名字和Class
增加新的WebModule。
...
procedure TWebModuleList.AddModule(AComponent: TComponent);
begin
Assert(FFixupLevel >= 1, 'Module created outside of fixup block'); { Do no
t localize }
FList.Add(AComponent);
if Assigned(FModuleAddedProc) then
FModuleAddedProc(AComponent);
end;
...
function TWebModuleList.AddModuleName(const AName: string): TComponent;
var
I: Integer;
Factory: TAbstractWebModuleFactory;
begin
Result := nil;
Assert(FindModuleName(AName) = nil);
for I := 0 to Factories.ItemCount - 1 do
begin
Factory := Factories.Items[I];
if CompareText(AName, Factory.ModuleName) = 0 then
begin
StartFixup;
try
Result := Factory.GetModule;
AddModule(Result);
break;
finally
EndFixup;
end;
end;
end;
end;
...
function TWebModuleList.AddModuleClass(
AClass: TComponentClass): TComponent;
var
I: Integer;
Factory: TAbstractWebModuleFactory;
begin
Result := nil;
Assert(FindModuleClass(AClass) = nil);
for I := 0 to Factories.ItemCount - 1 do
begin
Factory := Factories.Items[I];
if AClass = Factory.ComponentClass then
begin
StartFixup;
try
Result := Factory.GetModule;
AddModule(Result);
finally
EndFixup;
end;
end;
end;
end;
...
AddModule代码很简单,FModuleAddedProc是一个事件通知,可惜起了个怪怪
的名字,如果改名叫FOnModuleAdded应该更明朗一些,他通知感兴趣的客户端,有一个

新的WebModule被加入到此TWebModuleList实例中。
AddModuleName和AddModuleClass的代码很相似,区别是前者通过比较Factory
List中项目的名字而后者通过比较Class来取得合适的Factory,然后以GetModule方法
建立实例,然后调用AddModule。在这里我们可以看到Factory List的巨大作用。
而在建立WebModule List后,同样可以通过两种方法查找已有WebModule
...
function FindModuleClass(AClass: TComponentClass): TComponent; override;

 function FindModuleName(const AName: string): TComponent; override; 

...
代码很简单,我就不多说了,请自行查看代码。
另外TWebModuleList提供了自动化的WebModuleList构造、析构支持。
...
procedure AutoCreateModules;
procedure AutoDestroyModules;
...
procedure TWebModuleList.AutoCreateModules;
var
I: Integer;
Factory: TAbstractWebModuleFactory;
begin
StartFixup;
try
for I := 0 to Factories.ItemCount - 1 do
begin
Factory := Factories.Items[I];
if Factory.CreateMode = crAlways then
if FindModuleClass(Factory.ComponentClass) = nil then
AddModule(Factory.GetModule);
end;
finally
EndFixup;
end;
end;
...
procedure TWebModuleList.AutoDestroyModules;
var
I: Integer;
Factory: TAbstractWebModuleFactory;
Component: TComponent;
begin
for I := 0 to Factories.ItemCount - 1 do
begin
Factory := Factories.Items[I];
if Factory.CacheMode = caDestroy then
begin
Component := FindModuleClass(Factory.ComponentClass);
if Assigned(Component) then
Component.Free;
end;
end;
end;
...
这里顺便把TAbstractWebModuleFactory及其子类说一说。这个基类定义在HttpApp

单元中,一大把的virtual; abstract;方法,在WebReq中提供了一个简单的实现,
TDefaultWebModuleFactory,代码很简单,请大家自己阅读,只需记住他的CacheMode
为caCache;CreateMode为crAlways即可,因为我们马上要用到。至于WebFact里面提供

的更专业一些的实现,我们以后有机会再说。
...
TWebModuleCreateMode = (crOnDemand, crAlways);
TWebModuleCacheMode = (caCache, caDestroy);
...
AutoCreateModules遍历整个Factory List,查找每个Factory的CreateMode,
如果是crAlways则构造之;如果是crOnDemand,则等到以后需要使用时再构造。
而AutoDestroyModules则检查每个Factory的CacheMode,如果是caDestroy
则在ModuleList中找到相应实例析构之,如果是caCache则保留之,以便下次直接可用。

另外还有几个方法如下 

...
procedure EndFixup;
procedure StartFixup;
procedure RecordUnresolvedName(const AName: string);
procedure PromoteFactoryClass(const AName: string);
...
他们是为了修正VCL中一些潜在的问题而定制的,因为和我们的主题相关不大,
而且解释他们的实现原理需要涉及到很多VCL结构中比较低层的知识,几句话说不清
因此干脆跳过去算了。如果你有兴趣进一步了解,可以来信和我讨论 😃
在了解了两个List(Factory List & Module List)之后,再来看
TWebRequestHandler的实现代码就很简单了 😃
TWebRequestHandler中最重要的还是一个Factory List - FWebModuleFactories,
两个保存TWebModuleList的TList - FActiveWebModules, FInactiveWebModules
而真正的核心函数只有三个
...
function ActivateWebModules: TWebModuleList;
procedure DeactivateWebModules(WebModules: TWebModuleList);
function HandleRequest(Request: TWebRequest; Response: TWebResponse): Bo
olean;
...
让我们来一步一步介绍(痛苦啊,终于快说写了,手都敲累了,呵呵 😃
记得我刚刚说过,D5和D6都实现了Module Pooling,类似于以前介绍过的ISAPI实现

Thread Pooling的思路,FActiveWebModules和FInactiveWebModules分别保存活动的
和挂起的WebModuleList(D5是WebModule)实例。
...
function TWebRequestHandler.ActivateWebModules: TWebModuleList;
begin
if (FMaxConnections > 0) and (FAddingActiveModules >= FMaxConnections) the
n
raise Exception.CreateRes(@sTooManyActiveConnections);
FCriticalSection.Enter;
try
FAddingActiveModules := FActiveWebModules.Count + 1;
try
Result := nil;
if (FMaxConnections > 0) and (FActiveWebModules.Count >= FMaxConnectio
ns) then
raise Exception.CreateRes(@sTooManyActiveConnections);
if FInactiveWebModules.Count > 0 then
begin
Result := FInactiveWebModules[0];
Result.OnModuleAdded := nil;
FInactiveWebModules.Delete(0);
FActiveWebModules.Add(Result);
end
else
begin
if FWebModuleFactories.ItemCount = 0 then
if WebModuleClass <> nil then
FWebModuleFactories.AddFactory(TDefaultWebModuleFactory.Create(W
ebModuleClass));
if FWebModuleFactories.ItemCount > 0 then
begin
Result := TWebModuleList.Create(FWebModuleFactories);
FActiveWebModules.Add(Result);
end
else
raise Exception.CreateRes(@sNoDataModulesRegistered);
end;
finally
FAddingActiveModules := 0;
end;
finally
FCriticalSection.Leave;
end;
Result.AutoCreateModules;
end;
...
ActivateWebModules从池中激活一个WebModuleList并返回之。一开始检测是否
超过最大连接数,FMaxConnections在Create中设置缺省值为32,也就是说最多可以在
池中缓冲32个客户端WebModuleList。接下来以临界区FCriticalSection包裹主要代码
实现同步。然后又是检测最大连接数(真是麻烦的说,不知为何不把检测代码合并到一
起?)
接着看看池中(FInactiveWebModules)是否有可以直接激活使用的WebModuleList实例,

如果有,则将第一个可用实例取出放入返回值,并加入到活动WebModuleList列表中;
如果池中没有,则检测Factory List是否为空,为空则检测向后兼容的WebModuleClass

是否为空,不为空就将WebModuleClass的类型加入到Factory List中。
为了兼容D5代码,你使用的第一个WebModule的初始化代码如下
...
initialization
if WebRequestHandler <> nil then
WebRequestHandler.WebModuleClass := TWebDataModule1;
...
而如果再加入新的WebModule就会过渡到使用Factory的构建模型上
...
initialization
if WebRequestHandler <> nil then
WebRequestHandler.AddWebModuleFactory(
TWebDataModuleFactory.Create(TWebDataModule2, crOnDemand, caCache));
...
因此这里对Factory List如此处理。如果还是没有可用的Factory,则引发异常;
如果有,则建立一个新的TWebModuleList加入到活动列表中。最后调用AutoCreateModu
les
构造TWebModuleList里需要构造的WebModule。这部分我们刚刚讨论应该已经够详细了。

...
procedure TWebRequestHandler.DeactivateWebModules(WebModules: TWebModuleList
);
begin
FCriticalSection.Enter;
try
FActiveWebModules.Remove(WebModules);
WebModules.AutoDestroyModules;
if FCacheConnections and (WebModules.GetItemCount > 0) then
FInactiveWebModules.Add(WebModules)
else
begin
WebModules.Free;
end;
finally
FCriticalSection.Leave;
end;
end;
...
然后挂起TWebModuleList的代码就相对简单了,从活动列表FActiveWebModules
里Remove此WebModuleList,然后AutoDestroyModules之,如果TWebRequestHandler
允许缓存(FCacheConnections = True),并且WebModuleList内有没有析构的
WebModule(WebModules.GetItemCount > 0)也就是说不是所有的Factory设置
CacheMode为caDestroy,则将此WebModuleList加入到挂起列表
FInactiveWebModules中。
...
function TWebRequestHandler.HandleRequest(Request: TWebRequest;
Response: TWebResponse): Boolean;
var
I: Integer;
WebModules: TWebModuleList;
WebModule: TComponent;
WebAppServices: IWebAppServices;
GetWebAppServices: IGetWebAppServices;
begin
Result := False;
WebModules := ActivateWebModules;
if Assigned(WebModules) then
try
if WebModules.ItemCount = 0 then
raise Exception.CreateRes(@sNoWebModulesActivated);
try
// Look at modules for a web application
for I := 0 to WebModules.ItemCount - 1 do
begin
WebModule := WebModules[I];
if Supports(IInterface(WebModule), IGetWebAppServices, GetWebAppServ
ices) then
WebAppServices := GetWebAppServices.GetWebAppServices;
if WebAppServices <> nil then break;
end;
if WebAppServices = nil then
WebAppServices := TDefaultWebAppServices.Create(nil);
WebAppServices.InitContext(WebModules, Request, Response);
try
try
Result := WebAppServices.HandleRequest;
except
ApplicationHandleException(WebAppServices.ExceptionHandler);
end;
finally
WebAppServices.FinishContext;
end;
if Result and not Response.Sent then
Response.SendResponse;
except
ApplicationHandleException(nil);
end;
finally
DeactivateWebModules(WebModules);
end;
end;
...
最后就剩下一个HandleRequest了,我们前面几节讨论中,数据的处理最终都归结到

这个方法上来,让我们看看他是如何进行处理的。
首先是取得一个活动的WebModuleList,如果成功Assigned(WebModules)并且
有WebModule存在(WebModules.ItemCount <> 0)则遍历此WebModuleList,
向每个WebModule查询是否支持IGetWebAppServices接口,如果支持则使用
IGetWebAppServices.GetWebAppServices取得此WebModule的IWebAppServices
接口;如果都不支持则以缺省的TDefaultWebAppServices建立一个IWebAppServices。
然后以IWebAppServices初始化上下文(InitContext);处理请求(HandleRequest);

最后结束(FinishContext)。如果中途出现异常则调用ApplicationHandleException
处理,前面我们分析WebBroker时曾经谈到,ApplicationHandleException实际上被
初始化为HandleException方法
...
procedure TWebRequestHandler.HandleException(Sender: TObject);
var
Handled: Boolean;
Intf: IWebExceptionHandler;
begin
Handled := False;
if (ExceptObject is Exception) and
not (ExceptObject is EAbort) and
Supports(Sender, IWebExceptionHandler, Intf) then
Intf.HandleException(Exception(ExceptObject), Handled);
if not Handled then
SysUtils.ShowException(ExceptObject, ExceptAddr);
end;
...
如果HandleRequest成功并且返回信息没有被发送(not Response.Sent),
则发送返回信息Response.SendResponse。
剩下的一些方法,代码大多很简单,有这次分析WebReq和以前分析ISAPI的经验,
看懂应该不是难事,我就不多罗嗦了,呵呵。
而IWebAppServices等等一系列接口的结构与实现,则是以后小节的主要内容
我这里就先不多说了,呵呵,最后让我们总结一下 😃
不同的Web Server类型的实现,在处理完其类型相关的操作后,将输入和输出
的抽象包装类作为参数传递给TWebRequestHandler.HandleRequest方法,
此方法先检查最大缓冲连接数是否超过,没有,则试图从非活动WebModuleList列表
中取出现成的可用实例,如果没有则建立之。最终取得一个可用的实例。然后在此实例

的所有WebModule成员中查询并取得IWebAppServices接口(如都不支持则以缺省
TDefaultWebAppServices实现之),在取得接口后,通过接口的相关函数建立、
接触上下文环境,在此两项操作之间处理用户请求。
至此,Web Server程序的控制权已经从Web Application部分移交给了
Web Module部分,因为实际实现IWebAppServices接口的是某个WebModule,
我的系列文章也总算完成了他的第一部分。在接下来的章节中,我们将逐步接触真正
实现Web Server逻辑的Web Module部分。

预告 😃
五、Web Module部分的请求调度
从上一小节开始,用户请求已经从Web Application转向到Web Module部分
在座这个小节,让我们分析一下,用户请求是如何在Web Module本分析、调度、处理
用户编写的WebModule是如何相应客户请求的。
待续 😃

VCL架构下Web服务程序实现原理分析
Flier
目录 v0.1
标题 发表日期 最新修正版本
前言 2001.7.22 0.1 http://www.newsmth.net/nForum/#!article/Delphi/1388
一、整体结构及ISAPI实现 v0.1 2001.7.22 0.1 http://www.newsmth.net/nForum/#!article/Delphi/733
二、ISAPI实现中线程池原理与实现 2001.7.24 0.1 http://www.newsmth.net/nForum/#!article/Delphi/736
三、Web App的其他类型实现的分析 2001.7.30 0.1 http://www.newsmth.net/nForum/#!article/Delphi/743
四、WebBroker与WebReq的分析 2001.8.07 0.1 http://www.newsmth.net/nForum/#!article/Delphi/752
五、Web Module部分的请求调度 待定

                                 Flier Lu@白云黄鹤,2000.8.7 
                                 mailto:flier@126.com 
                                 telnet://bbs.whnet.edu.cn
posted @ 2018-05-19 19:07  c5soft  阅读(2359)  评论(0编辑  收藏  举报