利用ScktSrvr打造多功能Socket服务器.docx
《利用ScktSrvr打造多功能Socket服务器.docx》由会员分享,可在线阅读,更多相关《利用ScktSrvr打造多功能Socket服务器.docx(26页珍藏版)》请在冰豆网上搜索。
利用ScktSrvr打造多功能Socket服务器
利用ScktSrvr打造多功能Socket服务器
利用ScktSrvr打造多功能Socket服务器
时间:
2009-1-14来源:
作者:
本站编辑:
admin访问次数:
37【关闭】
Socket服务端编程中最重要的也是最难处理的工作便是客户请求的处理和数据的接收和发送,如果每一个Socket服务器应用程序的开发都要从头到尾处理这些事情的话,人将会很累,也会浪费大量时间。
试想,如果有一个通用的程序把客户请求处理和数据的接收、发送都处理好了,程序员只需要在不同的应用中对接收到的数据进行不同的解析并生成返回的数据包,再由这个通用程序将数据包传回客户端,这样,程序设计的工作将会轻松许多。
用Delphi进行过三层数据库应用开发的程序员一定对Borland公司的BorlandSocketServer(ScktSrvr.exe)不陌生。
这是一个典型的Socket服务器程序,认真读过该软件的源程序的人一定会赞叹其程序编写的高明。
其程序风格堪称典范。
但它是专用于配合Borland的MIDAS进行多层应用开发的。
它能不能让我们实现上面的设想,以便我们应用到不同的应用中去呢?
随我来吧,你会有收获的。
首先,让我们搞清楚它的工作方式和过程,以便看能不能用它完成我们的心愿,当然改动不能太大,否则我没耐心也没有能力去做。
从主窗体的代码开始:
不论是以系统服务方式启动程序或直接运行程序,当程序运行时,都会执行主窗体初始化方法:
TSocketForm.Initialize(FromService:
Boolean);
该方法代码简单易读,为节省篇幅在此不列出它的源代码。
该方法从注册表键“HKEY_LOCAL_MACHINE\SOFTWARE\Borland\SocketServer”中读取端口信息,每读到一个端口,则:
创建一个TSocketDispatcher的实例,并调用该实例的ReadSettings方法读取注册表数据来初始化该实例,然后激活该实例。
TSocketDispatcher继承自TServerSocket,是服务端Socket,当激活时便进入监听状态,监听客户端连接。
当有客户端连接时,触发TSocketDispatcher实例的GetThread事件过程:
procedureTSocketDispatcher.GetThread(Sender:
TObject;
ClientSocket:
TServerClientWinSocket;
varSocketThread:
TServerClientThread);
begin
SocketThread:
=TSocketDispatcherThread.Create(False,ClientSocket,
InterceptGUID,Timeout,SocketForm.RegisteredAction.Checked,SocketForm.AllowXML.Checked);
end;
该事件过程为每一个客户端连接创建一个TSocketDispatcherThread类的服务线程为该客户端服务,其核心过程就是TSocketDispatcherThread的ClientExecute方法。
对该方法的分析可以知道,它主要工作有两个:
一是创建一个传送器对象(TSocketTransport)负责与客户端进行数据传输,二是创建一个数据块解析器对象(TDataBlockInterpreter)负责解析传送器对象接收到的客户端请求数据包。
procedureTSocketDispatcherThread.ClientExecute;
var
Data:
IDataBlock;
msg:
TMsg;
Obj:
ISendDataBlock;
Event:
THandle;
WaitTime:
DWord;
begin
CoInitialize(nil);//初始化COM对象库
try
Synchronize(AddClient);//显示客户信息
FTransport:
=CreateServerTransport;//创建传送器对象,注意FTransport和下面的FInterpreter是线程对象的属性而不是局部变量
try
Event:
=FTransport.GetWaitEvent;
PeekMessage(msg,0,WM_USER,WM_USER,PM_NOREMOVE);//建立线程消息队列
GetInterface(ISendDataBlock,Obj);//获得TSocketDispatcherThread线程对象的ISendDataBlock接口
ifFRegisteredOnlythen
//创建数据块解析器对象,注意ISendDataBlock接口实例Obj作为参数传入了TDataBlockInterpreter的Create方法中
FInterpreter:
=TDataBlockInterpreter.Create(Obj,SSockets)else
FInterpreter:
=TDataBlockInterpreter.Create(Obj,'');
try
Obj:
=nil;
ifFTimeout=0then
WaitTime:
=INFINITEelse
WaitTime:
=60000;
whilenotTerminatedandFTransport.Connecteddo
try
caseMsgWaitForMultipleObjects(1,Event,False,WaitTime,QS_ALLEVENTS)of
WAIT_OBJECT_0:
begin
WSAResetEvent(Event);
Data:
=FTransport.Receive(False,0);//传送器对象接收客户端数据
ifAssigned(Data)then//接收成功
begin
FLastActivity:
=Now;
FInterpreter.InterpretData(Data);//数据块解析器对象对数据进行解析
Data:
=nil;
FLastActivity:
=Now;
end;
end;
WAIT_OBJECT_0+1:
whilePeekMessage(msg,0,0,0,PM_REMOVE)do
DispatchMessage(msg);
WAIT_TIMEOUT:
if(FTimeout>0)and((Now-FLastActivity)>FTimeout)then
FTransport.Connected:
=False;
end;
except
FTransport.Connected:
=False;
end;
finally
FInterpreter.Free;//释放数据块解析器对象
FInterpreter:
=nil;
end;
finally
FTransport:
=nil;//释放传送器对象
end;
finally
CoUninitialize;//关闭COM对象库
Synchronize(RemoveClient);//删除显示的客户信息
end;
end;
在代码中我们没有看到如何向客户端传回数据的过程,这项工作是由数据块解析器对象、传送器对象和接口ISendDataBlock(TSocketDispatcherThread实现了该接口)共同协调完成的。
从以上代码我们注意到,线程对象的ISendDataBlock接口(Obj变量)被作为参数传入了TDataBlockInterpreter的Create方法中,实际上也就是线程对象被传递到了数据块解析器对象中,后面我们将看到,数据块解析器完成数据解析后,会创建一个新的数据块(TDataBlock)对象来打包要返回到客户端的数据,然后调用ISendDataBlock接口的Send方法(实际上是TSocketDispatcherThread的Send方法)将数据发送到客户端,而TSocketDispatcherThread的Send方法最终调用传送器对象(TSocketDispatcherThread的FTransport)的Send方法进行实际的数据传输。
看下面的代码我们就清楚这一点:
{TSocketDispatcherThread.ISendDataBlock}
functionTSocketDispatcherThread.Send(constData:
IDataBlock;WaitForResult:
Boolean):
IDataBlock;
begin
//用传送器对象回传数据,其中Data是由数据块解析器创建的数据块对象,以接口类型参数的方式传到该函数
FTransport.Send(Data);
//当数据块解析器需要进行连续的数据回传(如数据太大,一次不能不能回传所有数据)时,
//它向WaitForResult参数传入True,SocketDispatcherThread就会
//在一次发送数据之后检索并解析客户端的回应,决定是否继续回传数据。
ifWaitForResultthen
whileTruedo
begin
Result:
=FTransport.Receive(True,0);//检索客户端回应
ifResult=nilthenbreak;
if(Result.SignatureandResultSig)=ResultSigthen
breakelse
FInterpreter.InterpretData(Result);//解析客户端回应
end;
end;
从上面的简单分析我们知道,在一次C/S会话过程中用到了几个对象,分别是:
传送器(TSocketTransport)对象,数据块解析器(TDataBlockInterpreter)对象,数据块(TDataBlock)对象,还有就是ISendDataBlock接口,它由TSocketDispatcherThread实现。
而数据处理主要在前两者,它们分工很明确,而这两者的协调就是通过后两者实现。
对象间的明确分工和有序合作给我们改造提供了条件。
再看离我们的设想有多远。
1、客户请求的处理:
TSocketDispatcher已经为我们做得很好了,这方面我们基本不需要改动。
2、数据的接收:
就看传送器能不能接收不同类型的数据了,若不能,再看方不方便派生和使用新的传送器类。
3、发送数据:
用TSocketDispatcherThread的Send方法就完成了,我们只需在解析请求后生成返回的数据块对象,传递给该方法就可以了。
4、解析数据:
不同的应用中对数据的解析肯定是不同的,只有用新的解析器类去实现,主要看在TSocketDispatcherThread的ClientExecute方法中能否应用不同的解析器类。
从接收数据开始。
数据接收由传送器(TSocketTransport)对象完成,该类在Sconnect单元中(请先将Sconnect单元做一个备份),我们看它的接收(Receive)方法:
functionTSocketTransport.Receive(WaitForInput:
Boolean;Context:
Integer):
IDataBlock;
var
RetLen,Sig,StreamLen:
Integer;
P:
Pointer;
FDSet:
TFDSet;
TimeVal:
PTimeVal;
RetVal:
Integer;
begin
Result:
=nil;
TimeVal:
=nil;
FD_ZERO(FDSet);
FD_SET(FSocket.SocketHandle,FDSet);
ifnotWaitForInputthen
begin
New(TimeVal);
TimeVal.tv_sec:
=0;
TimeVal.tv_usec:
=1;
end;
RetVal:
=select(0,@FDSet,nil,nil,TimeVal);
ifAssigned(TimeVal)then
FreeMem(TimeVal);
ifRetVal=SOCKET_ERRORthen
raiseESocketConnectionError.Create(SysErrorMessage(WSAGetLastError));
if(RetVal=0)thenExit;
//以上代码与Socket原理密切相关,功能是实现数据接收控制,本人理解还不是很透,也不需要改动它。
//以下代码才开始接收数据
RetLen:
=FSocket.ReceiveBuf(Sig,SizeOf(Sig));//检索数据签名
ifRetLen<>SizeOf(Sig)then
raiseESocketConnectionError.CreateRes(@SSocketReadError);//出错
CheckSignature(Sig);//检查数据标志,若不合法则产生异常
RetLen:
=FSocket.ReceiveBuf(StreamLen,SizeOf(StreamLen));//检索数据长度
ifRetLen=0then
raiseESocketConnectionError.CreateRes(@SSocketReadError);//出错
ifRetLen<>SizeOf(StreamLen)then
raiseESocketConnectionError.CreateRes(@SSocketReadError);//出错
Result:
=TDataBlock.CreateasIDataBlock;//创建数据块对象
Result.Size:
=StreamLen;//设置数据块对象的Size,即数据长度
Result.Signature:
=Sig;//设置数据块对象的数据标志
P:
=Result.Memory;//取得数据块对象的内存指针
Inc(Integer(P),Result.BytesReserved);//跳过保留字节数
whileStreamLen>0do//接收StreamLen字节的数据并写入数据块对象的数据域
begin
RetLen:
=FSocket.ReceiveBuf(P^,StreamLen);
ifRetLen=0then
raiseESocketConnectionError.CreateRes(@SSocketReadError);
ifRetLen>0then
begin
Dec(StreamLen,RetLen);
Inc(Integer(P),RetLen);
end;
end;
ifStreamLen<>0then
raiseESocketConnectionError.CreateRes(@SInvalidDataPacket);//出错
InterceptIncoming(Result);//如果采用了加密、压缩等处理过数据,在此将其还原
end;
分析到此,我们得先了解一下数据块对象,它并不复杂,因此在此不对其代码进行分析,只简单说明它的结构。
其实从MIDAS应用的客户端传来的请求就是一个数据块,上述接收过程将其接收后还原成一个数据块对象。
注意不要混淆数据块和数据块对象,前者是数据流,后者是一个对象,封装了数据块和对数据块操作的方法。
数据块的前8个字节(两个整数)为保留字节(BytesReserved=8),分别是数据块签名(Signature)和实际数据长度(Size),紧接着才是实际的数据,其长度由Size域指定。
数据块签名取值于一些预定义的常量,这些常量定义在SConnect单元中,如下:
const
{ActionSignatures}
CallSig=$DA00;//Callsignature
ResultSig=$DB00;//Resultsignature
asError=$01;//Specifyanexceptionwasraised
asInvoke=$02;//SpecifyacalltoInvoke
asGetID=$03;//SpecifyacalltoGetIdsOfNames
asCreateObject=$04;//Specifyacomobjecttocreate
asFreeObject=$05;//Specifyadispatchtofree
asGetServers=$10;//Getclassnamelist
asGetGUID=$11;//GetGUIDforClassName
asGetAppServers=$12;//GetAppServerclassnamelist
asSoapCommand=$14;//Soapcommand
asMask=$FF;//Maskforaction
从传送器的接收方法可看出,如果接收到的数据签名不合法,将引发异常,后续数据就不再接收。
再看下面对签名的检查:
procedureCheckSignature(Sig:
Integer);
begin
if(Sigand$FF00<>CallSig)and
(Sigand$FF00<>ResultSig)then
raiseException.CreateRes(@SInvalidDataPacket);
end;
签名的高字节必须为CallSig或ResultSig,满足这个条件就可通过接收检查这一关,后续数据就可正常接收。
签名的低字节由解析器解析,以实现不同的数据处理。
对数据签名的检查使得Scktsrvr.exe的应用范围局限于MIDAS应用。
如果我们要做成通用Socket服务器,比如做一个WWW服务器或做一个HTTP代理服务器,客户端(浏览器)发送来的请求(Http请求根本就不符合数据块的结构)是通不过检查的,连请求都无法接收,更谈不上处理了。
因此这是首先要改造的部分。
为了使服务器保留MIDAS的功能,又能用于其他Socket应用,我把数据传输分为MIDAS数据传输和自定义数据传输,如果是前者,接收方法自然不需变动,如果是后者,则跳过两个保留字节的接收,直接接收数据写到数据块对象中,至于数据解析,前面说过,是必须用新的解析器类的,我们在新的解析器中处理。
改造很简单:
1、给传送器类添加一个IsCustomTrans属性:
TSocketTransport=class(TInterfacedObject,ITransport)
private
...
FIsCustomTrans:
Boolean;{===MyCode===}
...
public
...
propertyIsCustomTrans:
BooleanreadFIsCustomTranswriteFIsCustomTrans;{===MyCode===}
end;
2、改写TSocketTransport的Receive方法:
functionTSocketTransport.Receive(WaitForInput:
Boolean;Context:
Integer):
IDataBlock;
var
RetLen,Sig,StreamLen:
Integer;
P:
Pointer;
FDSet:
TFDSet;
TimeVal:
PTimeVal;
RetVal:
Integer;
begin
...
if(RetVal=0)thenExit;
ifnotIsCustomTransthen{===MyCode===}
begin
RetLen:
=FSocket.ReceiveBuf(Sig,SizeOf(Sig));
...
ifRetLen<>SizeOf(StreamLen)then
raiseESocketConnectionError.CreateRes(@SSocketReadError);
end
else
StreamLen:
=FSocket.ReceiveLength;{===MyCode===}
Result:
=TDataBlock.CreateasIDataBlock;
ifnotIsCustomTransthen{===MyCode===}
Result.Signature:
=Sig;
...
end;
2、TSocketTransport的Send方法用于实际回传数据,也需改写:
functionTSocketTransport.Send(constData:
IDataBlock):
Integer;
var
P:
Pointer;
begin
Result:
=0;
InterceptOutgoing(Data);
P:
=Data.Memory;
ifIsCustomTransthen{===MyCode===}
FSocket.SendBuf(PByteArray(P)^[Data.BytesReserved],Data.Size){===MyCode===不发送保留字节}
else
FSocket.SendBuf(P^,Data.Size+Data.BytesReserved);
end;
到此,发送和接收的处理就改造完了,只用了几行代码,是不是很简单?
接下来要处理的是数据解析。
MIDAS的数据解析器类为TDataBlockInterpreter,它继承于TCustomDataBlockInterpreter。
这两个类也在Sconnect单元中,定义如下:
TCustomDataBlockInterpreter=class
protected
procedureAddDispatch(Value:
TDataDispatch);virtual;abstract;
procedureRemoveDispatch(Value:
TDataDispatch);virtual;abstract;
{