Delphi编写事件模型客户端(2)

2016-08-22 10:17:04来源:http://fxh7622.blog.51cto.com/63841/164247作者:fxh7622人点击


上次写了事件模型类的定义,今天我来写一写如何实现这个类。
首先的两个函数我想稍微了解网络编程的人都会清楚。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
procedure WSAStatupSocket;
var
WSData:TWSAData;
begin
if WSAStartup($0202, WSData) <> 0 then
begin
raise Exception.Create('WSAStartup Error.');
end;
end;
procedure WSACleanupSocket;
begin
if WSACleanup <> 0 then
begin
raise Exception.Create('WSACleanup Error.');
end;
end;
下来是真正类函数的实现。
procedure TIOEvents.ClearBuffer;
var
FNext:PSendBuffer;
begin
while Assigned(FFirstNode) do
begin
FNext:=FFirstNode.Next;
FreeMem(FFirstNode.Buf);
FFirstNode.BufLen:=0;
Dispose(FFirstNode);
Dec(FTotalCount);
FFirstNode:=FNext;
end;
if not Assigned(FFirstNode) then
begin
FLastNode:=nil;
end;
FTotalCount:=0;
end;
此函数用来对发送队列中的的数据清空,此函数在释放网络的时候使用。
创建和销毁函数的定义和实现如下:
constructor TIOEvents.Create;
begin
InitializeCriticalSection(FEventCS); //定义临界区
FMainIP := '127.0.0.1'; //定义服务端默认IP和默认端口
FMainPort := 5500;
FActive := false;
FKeepAlive := false; //初始化心跳不开启
FKeepTime := 1000;
FSendLen := DEFAULT_BUFSIZE; //初始化发送数据长度
FEventNums := 0;
end;
在创建函数中我对一些使用的变量进行了初始化。
在销毁函数中我将临界区进行释放。
destructor TIOEvents.Destroy;
begin
DeleteCriticalSection(FEventCS);
inherited;
end;

以下的两个函数是发送数据和接收数据中使用的主要函数。
首先是PostRecv函数,此函数用于投递接收。
function TIOEvents.PostRecv: Boolean;
var
Flags,RecvBytes: DWORD;
begin
Result:=true;
Flags := 0;
FRecvIOData.DataBuf.len := DATA_BUFSIZE;
ZeroMemory(@FRecvIOData.Buffer, sizeof(@FRecvIOData.Buffer));
FRecvIOData.DataBuf.buf := @FRecvIOData.Buffer;
if (WSARecv(FSocket, @(FRecvIOData.DataBuf), 1, @RecvBytes, @Flags, @(FRecvIOData.Overlapped), nil) = SOCKET_ERROR) then
begin
if (WSAGetLastError() <> ERROR_IO_PENDING) then
begin
Result:=false;
end;
end;
end;

其次是PostSend函数,此函数是投递发送,大家会注意到投递发送的时候的代码和投递接收数据的代码有些不同,这里的不同主要在于,我们每次投递发送的时候,数据的大小最大为4K的数据。对于比较长的数据,我会将它放入到发送队列中,所以投递的时候是从发送队列中的First数据,进行投递,并将First的Next数据设置为First。
function TIOEvents.PostSend: Boolean;
var
SendBytes: DWORD;
FNext:PSendBuffer;
begin
EnterCriticalSection(FEventCS);
try
Result:=true;
FillChar(FSendIOData.Buffer,SizeOf(FSendIOData.Buffer),#0);
ZeroMemory(@FSendIOData.Overlapped, sizeof(OVERLAPPED));
FNext:=FFirstNode.Next;
Move(FFirstNode.Buf^,FSendIOData.Buffer,FFirstNode.BufLen);
FSendIOData.BufferLen := FFirstNode.BufLen;
FSendIOData.DataBuf.len := FFirstNode.BufLen;
FSendIOData.DataBuf.buf := @FSendIOData.Buffer;
FSendIOData.Socket := FSocket;
if (WSASend(FSocket, @(FSendIOData.DataBuf), 1, @SendBytes, 0, @(FSendIOData.Overlapped), nil) = SOCKET_ERROR) then
begin
if (WSAGetLastError() <> ERROR_IO_PENDING) then
begin
Result:=false;
end;
end;
FreeMem(FFirstNode.Buf);
Dispose(FFirstNode);
FFirstNode:=FNext;
finally
LeaveCriticalSection(FEventCS);
end;
end;

下来是设置心跳函数,此函数如果看过我以前BLOG的朋友应该知道如何使用的。见《网络通信中的心跳机制的实现!》
function TIOEvents.SetKPAlive: Boolean;
var
inKeepAlive,OutKeepAlive:TTCP_KEEPALIVE;
opt:Integer;
insize,outsize,outByte:DWORD;
begin
Result :=true;
opt :=1;
if setsockopt(FSocket,SOL_SOCKET,SO_KEEPALIVE,@opt,sizeof(opt))=SOCKET_ERROR then
begin
Exit;
end;
inKeepAlive.onoff := 1;
inKeepAlive.keepalivetime := FKeepTime;
inKeepAlive.keepaliveinterval := 1;
insize := sizeof(TTCP_KEEPALIVE);
outsize := sizeof(TTCP_KEEPALIVE);
if WSAIoctl(FSocket,SIO_KEEPALIVE_VALS,@inKeepAlive,insize,@outKeepAlive,outsize,@outByte,nil,nil)=SOCKET_ERROR then
begin
Exit;
end;
end;

上面的PostSend函数只是一个投递发送数据的函数,可是如何将发送的时候放入发送队列呢?这个工作主要依靠一下的函数来实现。

function TIOEvents.SocketWrite(Data: Pchar; DataLen: Integer):Boolean;
var
iPos:Integer;
PNode:PSendBuffer;
begin
Result:=true;
if Assigned(Self) then
begin
if (DataLen<=0) or (FSendLen<DataLen) then
begin
Result:=false;
Exit;
end;
iPos:=0;
while DataLen - iPos > 0 do
begin
New(PNode);
if (DataLen - iPos)>=DATA_BUFSIZE then
begin
PNode.BufLen := DATA_BUFSIZE;
GetMem(PNode.Buf, DATA_BUFSIZE);
Move((Data+iPos)^, PNode.Buf^, DATA_BUFSIZE);
PNode.Next := nil;
Inc(iPos,DATA_BUFSIZE);
end
else
begin
PNode^.BufLen := DataLen - iPos;
GetMem(PNode.Buf, DataLen - iPos);
Move((Data+iPos)^, PNode.Buf^, DataLen - iPos);
PNode.Next := nil;
Inc(iPos,DataLen - iPos);
end;
//加入发送队列
EnterCriticalSection(FEventCS);
try
if not Assigned(FFirstNode) then
begin
FFirstNode:=PNode;
end
else
begin
FLastNode.Next:=PNode;
end;
FLastNode:=PNode;
Inc(FTotalCount);
finally
LeaveCriticalSection(FEventCS);
end;
if not Sending then
begin
Sending:=true;
if not PostSend then
begin
closesocket(FSocket);
break;
end;
end;
end;
end;
end;

由于要注意粘包缓存,所以发送的时候需要小于粘包函数大小,所以此函数的开始就是对发送数据的长度进行了判断。if (DataLen<=0) or (FSendLen<DataLen) then 当发送数据长度合适的时候,就需要将发送的数据分割成4k大小的数据块,并将此数据块放入到发送队列中。
当放置完毕以后,需要调用一次PostSend来发送数据。大家会看到在我调用PostSend函数之前对一个变量进行了判断。if not Sending then 这个变量Sending是用来判断当前是否正在发送数据,如果正在发送数据的话,那么就无需投递PostSend.
TIOEvents类的最后一个函数就是Start函数了,此函数用于创建、连接服务端、设置套接字的事件和创建一个工作者线程。
procedure TIOEvents.Start;
var
Addr:TSockAddr;
Event:Cardinal;
begin
FSocket := WSASocket(AF_INET, SOCK_STREAM, 0, nil, 0, WSA_FLAG_OVERLAPPED);
if FSocket = SOCKET_ERROR then
begin
closesocket(FSocket);
FSocket:=INVALID_SOCKET;
raise Exception.Create(Format('WSASocket Error ErrorID=%d',[GetLastError]));
Exit;
end;
Addr.sin_addr.s_addr:=inet_addr(Pchar(FMainIP));
Addr.sin_family:=AF_INET;
Addr.sin_port:=htons(FMainPort);
if (connect(FSocket,@Addr,sizeof(Addr))=SOCKET_ERROR) then
begin
closesocket(FSocket);
FSocket:=INVALID_SOCKET;
raise Exception.Create(Format('connect Error ErrorID=%d',[GetLastError]));
Exit;
end;
if FKeepAlive then
begin
SetKPAlive;
end;
Event:=WSACreateEvent;
FillChar(FRecvIOData,SizeOf(FRecvIOData),0);
FillChar(FSendIOData,SizeOf(FSendIOData),0);
FEventArray[FEventNums] :=Event;
FSocketArray[FEventNums]:=FSocket;
FRecvIOData.Overlapped.hEvent := Event;
FSendIOData.Overlapped.hEvent := Event;
Inc(FEventNums);
//注册此套接字上的事件
if WSAEventSelect(FSocket,Event, FD_READ or FD_WRITE or FD_CLOSE) = SOCKET_ERROR then
begin
raise Exception.Create(Format('WSAEventSelect Error ErrorID=%d',[GetLastError]));
Exit;
end;
//创建工作者线程
FWorkThread:=TWorkThread.Create(Self);
end;

工作者线程是发送和接收数据的主要部分。没有这部分代码,将无法实现网络通信。下面一篇我会写出我是如何编写工作者线程的。

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台