//先贴几个实用函数
//16 进制 转 整形,这个在消息解码的时候经常要用到
Function HexToInt(sTemp : String) : Integer ;
Var
V, i : Integer ;
Begin
HexToInt := 0 ;
If sTemp = '' Then Exit ;
If (sTemp[1]='0') And ((sTemp[2]='x') Or (sTemp[2]='X')) Then
sTemp := Copy(sTemp, 3, Length(sTemp)-2) ;
If sTemp = '' Then Exit ;
V := 0 ;
For i := 1 To Length(sTemp) Do
Case sTemp[i] Of
'0'..'9': V:=V*16+Ord(sTemp[i])-Ord('0') ;
'A'..'F': V:=V*16+Ord(sTemp[i])-Ord('A')+10 ;
'a'..'f': V:=V*16+Ord(sTemp[i])-Ord('a')+10 ;
End ;
HexToInt := V ;
End ;
//BCD码 和 字符串 互转 的函数,这两个函数几乎是必备的
//BCD码 转 字符串
Function BCD2Str(P : PChar ; Pos, Len : integer) : string;
Var
i : integer;
strResult : string;
cRead : Byte ;
cReadH,cReadL : Byte;
bOdd : boolean;
Begin
strResult := '' ;
if (Len mod 2)= 1 then
begin
Len := (Len + 1) div 2;
bOdd := true;
end else begin
Len := Len div 2;
bOdd := false;
end;
For i := 0 To Len-1 Do
Begin
cRead := Ord(P[pos+i]);
cReadH := (cRead and $f0) shr 4;
strResult := strResult + IntToHex(cReadH,1);
if (i=Len-1) and bOdd then continue;
cReadL := cRead and $0f;
strResult := strResult + IntToHex(cReadL,1);
End ;
BCD2Str := strResult;
end;
//字符串 转 BCD码
function Str2Bcd(Str : String): String;
var
i,Bytes : Byte;
Odd : boolean;
Hex : String;
begin
Result := '';
Odd := Length(Str) mod 2 = 1;
Bytes := (Length(Str) + 1) div 2;
if Odd then Str := Str + '0';
for i:=0 to Bytes-1 do
begin
Hex := Copy(Str,i*2+1,2);
Result := Result + chr(HexToInt(Hex));
end;
end;
//SOCKET通信编程中经常需要组装消息,因此 Move 函数的使用频率非常高,通常情况下是直接将封好的结构直接MOVE到一个BUFFER中,但是有些情况下我们想MOVE一个INTEGER值到消息的某个字段中,使用MOVE函数时你会发现低位在前,高位在后,这和我们所希望的正好相反。使用这个函数则没有问题。
procedure MoveInt(Size : Byte; const Value; var Buf);
var
Hex,BufStr : String;
begin
Hex := IntToHex(Integer(Value),Size*2);
BufStr := Str2Bcd(Hex);
Move(BufStr[1],Buf,Length(BufStr));
end;
//通常我们编写的通信程序是在网络状况接近于理想状态的局域网中运行,因此你大可在接收端直接使用 Socket.ReceiveBuf 来收取一整条消息,这时候消息的结构定义显得不是很重要,但是如果我们写的软件是要在INTERNET上通信,这个时候消息结构的定义就显的非常重要了,因为在互联网上消息的传送不像局域网那样顺畅,接收端在收取一条完整消息时可能需要分多次才能收全。
//例如发送端使用 SendBuf 方法发送内容为‘123456’的消息,则接收端在 ReceiveBuf 时可能先收到‘123’然后再次 ReceiveBuf 时才能收到‘456’。这时候必须要定义清楚消息头和消息尾,消息数据区的转码方法等。程序必须要有一个消息缓存用来记录先前收到的字节流,然后当再次收到字节流后尝试检测消息尾,将其拼装成一条完整的消息。如果无法拼装则丢弃该消息,不做进一步处理。
//下面的代码实现了这一拼装过程
const
MSG_HEAD = #$FE ;
MSG_TAIL = #$EF ;
MSG_TRSF = #$FD ;
CFD = #$FD;
CFE = #$FE;
CEF = #$EF;
CFDFlag = #$00;
CFEFlag = #$01;
CEFFlag = #$02;
var
CacheBuf : array[0..255] of char // 全局消息缓存
bMsgStart : boolean; // 消息起始标志位
bMsgEnd : Boolean; // 消息结束标志位
BufLen : Integer; // 当前消息长度
procedure SomeProcedure;
var
i : Integer;
begin
FRecvLen := FSock.ReceiveLength;
FMsgBuf := #0;
FSock.ReceiveBuf(FMsgBuf,FRecvLen);
while i < FRecvLen do
begin
if FMsgBuf[i] = MSG_HEAD then
begin
bMsgStart := True;
bMsgEnd := False;
CacheBuf := #0;
BufLen := 0;
end else if FMsgBuf[i] = MSG_TAIL then
bMsgEnd := True;
if bMsgStart then
begin
if FMsgBuf[i] <> MSG_TRSF then
Move(FMsgBuf[i],CacheBuf[BufLen],1)
else begin
case FMsgBuf[i+1] of
CFDFlag : CacheBuf[BufLen] := CFD;
CFEFlag : CacheBuf[BufLen] := CFE;
CEFFlag : CacheBuf[BufLen] := CEF;
end;
Inc(i);
end;
Inc(BufLen);
end;
if bMsgStart and bMsgEnd then
begin
//ProcessMsg; 在这里加入具体的消息处理
bMsgStart := false;
end;
Inc(i);
end;
end;
//发送消息时我们需要对消息数据区进行转码,即将数据区中和消息头消息尾相同的字节转换成其他字节,使程序能区分出真正的消息头和消息尾。
//下面的函数实现了转码
type
TSysMsgBuf = array[0..255] of char;
TMsgPool = record
SendBuf : TSysMsgBuf ;
Len : Byte;
end;
function EnCodeSysMsg(var TmpBuf : TSysMsgBuf; TmpBufLen : Integer): TMsgPool;
var
i,InFactCount:integer;
begin
Result.SendBuf := #0;
Result.SendBuf[0] := MSG_HEAD ;
InFactCount := 1;
for i:=1 to TmpBufLen do
begin
Case TmpBuf[i] of
MSG_HEAD:
begin
Result.SendBuf[InFactCount] := CFD;
inc(InFactCount);
Result.SendBuf[InFactCount] := CFEFlag;
end;
CFD:
begin
Result.SendBuf[InFactCount] := CFD;
inc(InFactCount);
Result.SendBuf[InFactCount] := CFDFlag;
end;
MSG_TAIL:
begin
Result.SendBuf[InFactCount] := CFD;
inc(InFactCount);
Result.SendBuf[InFactCount] := CEFFlag;
end;
else
Result.SendBuf[InFactCount] := TmpBuf[i];
end;
Inc(InFactCount);
end;
Result.SendBuf[InFactCount] := MSG_TAIL ;
Result.Len := InFactCount+1;
end;
//为了保证我们的通信程序有相当的健壮性,在遇到不可预测的网络线路问题时需要保持收发两端的状态一致,我们必须在服务端同客户端的消息交互中加入心跳机制。
//其实这很简单:比如服务端A和客户端B建立链路并开始了正常通信,此时服务端开启一个定时器,每隔一定时间向客户端发送一条固定内容的消息(即心跳消息),客户端在收到这条消息立即相应的回复一条固定内容的消息(即心跳应答消息),服务器端 和 客户端 都将对未收到心跳消息的次数进行累计,如果达到定义的最大值则认为线路异常,主动切断同对端的连接。要知道SOCKET组件的ONERROR事件在对端是无法触发的。通过加入心跳的方法可以保证两边的状态保持一直,不会出现所谓的“假登录”。你可以通过拔插网线的方法来进行测试。
//下面的代码演示了这一机制
procedure TfrmMain.tmrHBTimer(Sender: TObject);
var
i : Integer;
Client : PCBClient;
Sock : TCustomWinSocket;
procedure SendHb;
var
Hb_Buf : TSysMsgBuf;
begin
Hb_Buf := BuildMsg_Hb(iModuleId);
Sock.SendBuf(HB_Buf,CMsgLen_Hb+2);
end;
begin
with Clients.LockList do
begin
try
for i:=0 to Count-1 do
begin
Client := Items[i];
Sock := TCustomWinSocket(Client.Thread);
with Client^ do
if RegFlag then
begin
if HbAckFlag then
begin
RecvNoHbCt := 0;
SendHb;
HbAckFlag := False;
end else begin
Inc(RecvNoHbCt);
if RecvNoHbCt > iMaxRetryCt then
begin
AddClientLog(Host,'用户失去响应'+IntToStr(iMaxRetryCt)+'次,断开连接');
Sock.Close;
end else begin
SendHb;
CBHandler.SetClient(Client);
CBHandler.GetBill;
AddClientLog(Host,'用户失去响应,重试 '+IntToStr(RecvNoHbCt));
end;
end;
end else begin
Inc(RecvNoLogCt);
if RecvNoLogCt > 3 then
begin
AddClientLog(Host,'等待登录消息超时,断开连接');
Sock.Close;
end;
end;
end;
finally
Clients.UnlockList;
end;
end;
end;
// 使用类来封装消息,一个通信系统中定义的消息体系应该具有明显的继承特性,比如所有消息都有公共的消息头部分 即 msg_header 但有各自不同消息类型字段和数据区以及各自的编解码方法,有些消息只是扩展了某些消息的预留字段,使用类来进行封装是最好的选择,OOP中的多态将可以被充分利用。
//如果系统比较复杂,尽可能将消息处理过程也封装为类,并且写一个基类来处理最基本的消息的接收和拼装,就像下面这样,这样做的好处不仅使你的程序更加结构话,而且将业务逻辑和程序组件以及界面完全分开,当你需要修改逻辑的时候,界面部分的代码可以完全不用改动。
type
TDMMsgHandler = class // 数据同步消息处理器基类
private
FPN : String;
FDN : String;
FCalled : String;
FStartTime : TDateTime;
FEndTime : TDateTime;
FDuration : Integer;
FSerial : Integer;
//
FMsgBuf : TSysMsgBuf;
FSock : TCustomWinSocket;
FRecvLen : Integer;
FHeader : TMsgHeader;
FData : TMsgData;
//
FBufToSend : TSysMsgBuf;
FSockPool : TMsgPool;
public
constructor Create(Sock : TCustomWinSocket);
function SetMsgBuf : boolean;
procedure Process; virtual ; abstract;
published
property _PN : String read FPN;
property _DN : String read FDN;
property _Called : String read FCalled;
property _StartTime : TDateTime Read FStartTime;
Property _EndTime : TDateTime Read FEndTime ;
property _Duration : Integer read FDuration ;
property _Serial : Integer read FSerial;
end;
TDMserMsgHandler = class(TDMMsgHandler) // 服务端数据同步处理器
private
FBcg_Id : Byte;
FEx_Id : Byte;
FCB_Flag : Byte;
FTrunk_In_Id : Integer;
FTrunk_Out_Id : Integer;
public
procedure Process; override;
function GetDmBill : boolean;
procedure SendDmBill;
function UpDateDmFlag : boolean;
procedure ShowDMrec;
published
property _CB_Flag : Byte read FCB_Flag;
property _Bcg_Id : Byte read FBcg_Id;
property _Ex_Id : Byte read FEx_Id;
property _Trunk_In_Id : Integer read FTrunk_In_Id;
property _Trunk_Out_Id : Integer read FTrunk_Out_Id;
end;
// 以上是我在项目实践中总结的一些要点,其中有些也是通信行业SOCKET通信程序设计的不成文标准。由于本人目前就职的公司是上海贝尔的子公司,专门开发以电信业务为核心的通信类软件,所以公司在这方面是相当有经验的,其中许多的经验已经成为“定式”。
//需要点明的是由于 Delphi 开发的 C/S 通信程序是单线程的,所以系统的用户容量最好在 100 以内,否则可能会发生问题,除非你抛弃 DELPHI 自带的 socket 组件,使用WinSock自行重新封装。D7 INDY 中的 TCPserver/TCPclient 组件虽然是多线程的,但是由于是第3方组件,仍然有许多BUG。大型的通信应用必须用C系列来开发,如现在通信行业中最普遍被使用的ACE。C的效率是DELPHI所无法比拟的,尤其在通信行业中