上次寫了事件模型類的定義,今天我來寫一寫如何實現這個類。
首先的兩個函數我想稍微瞭解網絡編程的人都會清楚。
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;
工作者線程是發送和接收數據的主要部分。沒有這部分代碼,將無法實現網絡通信。下面一篇我會寫出我是如何編寫工作者線程的。