原处:http://fxh7622.blog.51cto.com/63841/7667
使用IOCP的时候需要使用的结构:
(1):单IO数据结构
LPVOID = Pointer;
LPPER_IO_OPERATION_DATA = ^ PER_IO_OPERATION_DATA ;
PER_IO_OPERATION_DATA = packed record
Overlapped: OVERLAPPED;
DataBuf: TWSABUF;
Buffer: array [0..1024] of CHAR;
BytesSEND: DWORD;
BytesRECV: DWORD;
end;
上面的结构中 OVERLAPPED 声明为:
typedef struct OVERLAPPED {
DWORD Internal; // 系统状态,保留
DWORD InternalHigh; // 传输数据的长度, 保留 , 当 GetOverlappedResult = True 时
DWORD Offset; // 偏移量 , 当从管道和通讯设备读写数据时忽略
DWORD OffsetHigh; // 当从管道和通讯设备读写数据时忽略
HANDLE hEvent; // 事件的ID
};
WSABUF= packed record
{
len : longword; // buffer 长度
buf : pchar; // buffer 指针
}
Buffer: array [0..1024] of CHAR;是用来保存接受数据的缓存。
BytesSEND: DWORD;用来标志发送数据的长度。
BytesRECV: DWORD;用来标志接受数据的长度。
因为完成端口的工作者线程可以接受到来自客户端的数据,同时还可以接受到自己发送给客户端的数据,所以我们使用BytesSEND,BytesRECV变量来说是用来区分这次的数据是来自客户端的数据还是自己发送出去的数据。
(2):“单句柄数据结构”
LPPER_HANDLE_DATA = ^ PER_HANDLE_DATA;
PER_HANDLE_DATA = packed record
Socket: TSocket;
end;
一个完成端口的实例如下:
1. 加载SOCKET. 使用的是2.2版为了后面方便加入心跳:
if WSAStartUp($202, wsData) <> 0 then
begin
WSACleanup();
end;
WSAStartUp:
2. 创建一个完成端口:
CompletionPort:=CreateIOCompletionPort(INVALID_HANDLE_VALUE,0,0,0);
3. 根据CPU的数量创建CPU*2数量的工作者线程:
GetSystemInfo(LocalSI);
for I:=0 to LocalSI.dwNumberOfProcessors * 2 -1 do
begin
hThread := CreateThread(nil, 0, @ServerWorkerThread, Pointer(CompletionPort),0, ThreadID);
if (hThread = 0) then
begin
Exit;
end;
CloseHandle(hThread);
end;
5. 创建一个套接字,将此套接字和一个端口绑定并监听此端口:
Listensc:=WSASocket(AF_INET,SOCK_STREAM,0,Nil,0,WSA_FLAG_OVERLAPPED);
if Listensc=SOCKET_ERROR then
begin
closesocket(Listensc);
WSACleanup();
end;
sto.sin_family:=AF_INET;
sto.sin_port:=htons(5500);
sto.sin_addr.s_addr:=htonl(INADDR_ANY);
if bind(Listensc,sto,sizeof(sto))=SOCKET_ERROR then
begin
closesocket(Listensc);
end;
listen(Listensc,20);
while (TRUE) do
begin
Acceptsc:= WSAAccept(Listensc, nil, nil, nil, 0);
//当客户端有连接请求的时候,WSAAccept函数会新创建一个套接字Acceptsc。这个套接字就是和客户端通信的时候使用的套接字。
if (Acceptsc= SOCKET_ERROR) then
begin
closesocket(Listensc);
exit;
end;
6. 判断Acceptsc套接字创建是否成功,如果不成功则退出。
PerHandleData := LPPER_HANDLE_DATA (GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)));
if (PerHandleData = nil) then
begin
exit;
end;
PerHandleData.Socket := Acceptsc;
7. 创建一个“单句柄数据结构”将Acceptsc套接字绑定。
if (CreateIoCompletionPort(Acceptsc, CompletionPort, DWORD(PerHandleData), 0) = 0) then
begin
exit;
end;
8. 将套接字、完成端口和“单句柄数据结构”三者绑定在一起。
PerIoData := LPPER_IO_OPERATION_DATA(GlobalAlloc(GPTR, sizeof(PER_IO_OPERATION_DATA)));
if (PerIoData = nil) then
begin
exit;
end;
ZeroMemory(@PerIoData.Overlapped, sizeof(OVERLAPPED));
PerIoData.BytesSEND := 0;
PerIoData.BytesRECV := 0;
PerIoData.DataBuf.len := 1024;
PerIoData.DataBuf.buf := @PerIoData.Buffer;
Flags := 0;
9.创建一个“单IO数据结构”其中将PerIoData.BytesSEND 和PerIoData.BytesRECV 均设置成0。说明此“单IO数据结构”是用来接受的。
if (WSARecv(Acceptsc, @(PerIoData.DataBuf), 1, @RecvBytes, @Flags,@(PerIoData.Overlapped), nil) = SOCKET_ERROR) then
begin
if (WSAGetLastError() <> ERROR_IO_PENDING) then
begin
//最近在检查代码的时候发现以前这里只是使用Exit来退出是不正确的。这里需要删除申请的单IO数据结构,否子会出现内存泄露。 (2008年3月24日)
//Exit;
closesocket(AcceptSc);
if PerIoData <> nil then
begin
GlobalFree(DWORD(PerIoData));
end;
Continue;
end
end;
用此“单IO数据结构”来接受Acceptsc套接字的数据。
end;
创建IOCP的工作已经完成.
接受到客户端发送过来和自己发送出去的数据都是从工作者线程中得到。
10. IOCP的工作者线程的处理方法:
function ServerWorkerThread(CompletionPortID:Pointer):Integer;stdcall;
begin
CompletionPort:=THANDLE(CompletionPortID);
//得到创建线程是传递过来的IOCP
while(TRUE) do
begin
//工作者线程会停止到GetQueuedCompletionStatus函数处,直到接受到数据为止
if (GetQueuedCompletionStatus(CompletionPort, BytesTransferred,DWORD(PerHandleData), POverlapped(PerIoData), INFINITE) = False) then
begin
//当客户端连接断开或者客户端调用closesocket函数的时候,函数GetQueuedCompletionStatus会返回错误。如果我们加入心跳后,在这里就可以来判断套接字是否依然在连接。
if PerHandleData<>nil then
begin
closesocket(PerHandleData.Socket);
GlobalFree(DWORD(PerHandleData));
end;
if PerIoData<>nil then
begin
GlobalFree(DWORD(PerIoData));
end;
continue;
end;
if (BytesTransferred = 0) then
begin
//当客户端调用shutdown函数来从容断开的时候,我们可以在这里进行处理。
if PerHandleData<>nil then
begin
TempSc:=PerHandleData.Socket;
shutdown(PerHandleData.Socket,1);
closesocket(PerHandleData.Socket);
GlobalFree(DWORD(PerHandleData));
end;
if PerIoData<>nil then
begin
GlobalFree(DWORD(PerIoData));
end;
continue;
end;
//在上一篇中我们说到IOCP可以接受来自客户端的数据和自己发送出去的数据,两种数据的区别在于我们定义的结构成员BytesRECV和BytesSEND的值。所以下面我们来判断数据的来自方向。因为我们发送出去数据的时候我们设置了结构成员BytesSEND。所以如果BytesRECV=0同时BytesSEND=0那么此数据就是我们接受到的客户端数据。
if (PerIoData.BytesRECV = 0) and (PerIoData.BytesSEND = 0) then
begin
PerIoData.BytesRECV := BytesTransferred;
PerIoData.BytesSEND := 0;
end
else
begin
PerIoData.BytesSEND := BytesTransferred;
PerIoData.BytesRECV := 0;
end;
//当是接受来自客户端的数据是,我们进行数据的处理。
if (PerIoData.BytesRECV > PerIoData.BytesSEND) then
begin
PerIoData.DataBuf.buf := PerIoData.Buffer + PerIoData.BytesSEND;
PerIoData.DataBuf.len := PerIoData.BytesRECV - PerIoData.BytesSEND;
//这时变量PerIoData.Buffer就是接受到的客户端数据。数据的长度是PerIoData.DataBuf.len 你可以对数据进行相关的处理了。
//.......
//当我们将数据处理完毕以后,应该将此套接字设置为结束状态,同时初始化和它绑定在一起的数据结构。
ZeroMemory(@(PerIoData.Overlapped), sizeof(OVERLAPPED));
PerIoData.BytesRECV := 0;
Flags := 0;
ZeroMemory(@(PerIoData.Overlapped), sizeof(OVERLAPPED));
PerIoData.DataBuf.len := DATA_BUFSIZE;
ZeroMemory(@PerIoData.Buffer,sizeof(@PerIoData.Buffer));
PerIoData.DataBuf.buf := @PerIoData.Buffer;
if (WSARecv(PerHandleData.Socket, @(PerIoData.DataBuf), 1, @RecvBytes, @Flags,@(PerIoData.Overlapped), nil) = SOCKET_ERROR) then
begin
if (WSAGetLastError() <> ERROR_IO_PENDING) then
begin
if PerHandleData<>nil then
begin
TempSc:=PerHandleData.Socket;
closesocket(PerHandleData.Socket);
GlobalFree(DWORD(PerHandleData));
end;
if PerIoData<>nil then
begin
GlobalFree(DWORD(PerIoData));
end;
continue;
end;
end;
end
//当我们判断出来接受的数据是我们发送出去的数据的时候,在这里我们清空我们申请的内存空间
else
begin
GlobalFree(DWORD(PerIoData));
end;
end;
end;
到此,工作者线程已经处理完成。
11. 如何使用IOCP发送数据:
function TNetControl.SendSpecifyData(const Socket: TSocket; Data: array of char;
DataLen: Integer): Boolean;
const
DATA_BUFSIZE = 1024; //这里定义一个发送数据的缓存长度,只要和接收的一直就可以
var
PerIoData: LPPER_IO_OPERATION_DATA ;
SendBytes, RecvBytes: DWORD;
Flags: DWORD ;
LenStr:String;
SendBuf:array [0..DATA_BUFSIZE] of char;
begin
try
//由于粘包的关系,所以在需要发送的数据前面加入4位这次发送数据的长度。(详见我的前一篇文章)
SetArrayLength(DataLen,LenStr) ;
Fillchar(SendBuf,sizeof(SendBuf),#0);
strmove(SendBuf,Pointer(LenStr),4);
strmove(SendBuf+4,Data,DataLen);
//在这里申请一个发送数据的"单IO数据结构"
PerIoData := LPPER_IO_OPERATION_DATA(GlobalAlloc(GPTR, sizeof(PER_IO_OPERATION_DATA)));
if (PerIoData = nil) then
begin
Result:=false;
exit;
end;
ZeroMemory(@PerIoData.Overlapped, sizeof(OVERLAPPED));
//设置发送标记
PerIoData.BytesRECV := 0;
PerIoData.DataBuf.len := DataLen+4;
PerIoData.DataBuf.buf:=@SendBuf;
PerIoData.BytesSEND := DataLen+4;
Flags := 0;
//使用WSASend函数将数据发送
if (WSASend(Socket, @(PerIoData.DataBuf), 1, @SendBytes, 0,@(PerIoData.Overlapped), nil) = SOCKET_ERROR) then
begin
if (WSAGetLastError() <> ERROR_IO_PENDING) then
begin
//最近在检查代码的时候发现以前这里只是使用Exit来退出是不正确的。这里需要删除申请的单IO数据结构,否子会出现内存泄露。 (2008年3月24日)
//Exit;
//表示发送失败,以后也不会有处理在工作者线程处出现。
if PerIoData <> nil then
begin
GlobalFree(DWORD(PerIoData));
end;
Result:=false;
Exit;
end;
end;
Result:=true;
except
Result:=false;
end;
end;
使用IOCP发送数据的代码就这些,但是这里需要说明一些问题。
1:一定发送我们在申请了“单IO数据结构”以后并没有对它进行释放。这是因为我们使用的是异步函数WSASend来进行发送数据,只有当我们确定将数据发送出去以后才可以将我们申请的这个结构释放。这就引出了第二个问题。
2:如何判断我们发送的数据已经发送。区分这个数据是来自客户端还是自己发送出去的区分就是使用PerIoData.BytesRECV 和PerIoData.BytesSEND 如果PerIoData.BytesSEND >0则表示这个数据是自己发送出去的。现在咱们来回顾一下以前的代码,找出释放“单IO数据结构”的地方。
//当我们判断出来接受的数据是我们发送出去的数据的时候,在这里我们清空我们申请的内存空间
else
begin
GlobalFree(DWORD(PerIoData));
end;
这里就是释放“单IO数据结构”的地方。
到此,整个的IOCP从创建、初始化、接收和发送过程就结束了。