如何安全地停止 IOCP WSARecv() 任务,并释放 WSAOVERLAPED 结构?

How to safely stop a IOCP WSARecv() task, and free the WSAOVERLAPPED structure?

本文关键字:任务 释放 WSAOVERLAPED 结构 WSARecv 何安全 安全 IOCP      更新时间:2023-10-16

我的 IOCP 服务器程序在运行时会消耗越来越多的内存。在跟踪内存泄漏后,我发现一些WSAOVERLAP结构馈送到WSARecv((从未被回收。我认为这是因为某些恶性客户端套接字只建立连接,但从不发送数据或关闭。所以我在每个客户端套接字上设置了一个 TimerQueueTimer(( 来识别超时套接字并删除它们。但是如果我在删除恶性套接字时释放了WSAOVERLAP结构,过了一会儿,我得到了"释放后在014C7D80处修改了自由堆块014C7D80"。

以下是一些相关代码:

typedef struct _SocketState
{
char operation; 
SOCKET socket;   
DWORD length;
HANDLE hTimer;
HANDLE hCompletion;
WSAOVERLAPPED* thisOvl;
char buf[MAX_BUF];
} SocketState;
static WSAOVERLAPPED* new_overlapped(void)
{
return (WSAOVERLAPPED*)calloc(1, sizeof(WSAOVERLAPPED));
}
static void create_io_completion_port(void)
{
cpl_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (!cpl_port)
{
int err = WSAGetLastError();
exit(1);
}
}
static void post_reading(SocketState* socketState, WSAOVERLAPPED* ovl)
{
DWORD flags = 0;
WSABUF wsabuf = { MAX_BUF, socketState->buf };
int err=0;
memset(ovl, 0, sizeof(WSAOVERLAPPED));
socketState->operation = OP_READ;
socketState->thisOvl=ovl;
if (WSARecv(socketState->socket, &wsabuf, 1, NULL, &flags, ovl, NULL)== SOCKET_ERROR)
{
err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
printf("[%s:%d]WSARecv errorn", __FUNCTION__, __LINE__);
destroy_connection(socketState, ovl);
return ;
}
}
c_WSARecv++;
}
static void destroy_connection(SocketState* socketState, WSAOVERLAPPED* ovl)
{
int err=0;
if(socketState->hTimer != NULL)
{
DeleteTimerQueueTimer(hTimerQueue,socketState->hTimer,INVALID_HANDLE_VALUE);
socketState->hTimer = NULL; 
}
socketState->hCompletion=NULL; //newSocketState->hCompletion = cpl_port
closesocket(socketState->socket);
free(socketState);
if(ovl!=0)
{
free(ovl);
}
}
VOID CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
SocketState* clientSocketState=(SocketState*)lpParam;
if (lpParam != NULL)
{
if(clientSocketState->hCompletion != NULL)
{
PostQueuedCompletionStatus(clientSocketState->hCompletion,-2,(ULONG_PTR)clientSocketState,clientSocketState->thisOvl);
//should last parameter be NULL?
//the "-2" is for identify this timeout io after GetQueuedCompletionStatus()
}
}
}

由于我在服务器程序中使用 C 而不是 C++,因此我让自己处于非常尴尬的境地。基本上我在 C :( 中找不到非常好的 IOCP 示例。

OVERLAPPED

我们只能在 I/O 完成后释放。 所以真的所有需要取消I/O操作。 这可以通过调用CancelIoEx来完成,或者通过调用closesocket完成更好(closesocket函数将对未完成的 I/O 操作启动取消(。 当 I/O 完成时 - 你得到了指向OVERLAPPED传递到此 I/O 的指针,在处理 I/O 结果之后 - 你可以释放或重用OVERLAPPED


您有与每个套接字关联的结构 -SocketState这是正确的。 但是什么必须和不能有这种结构?

它必须实现引用计数(因为它以复杂且不可预测的顺序从多个线程访问(。 并按住套接字的手柄。 强制对套接字句柄进行某种破损保护,以便在closesocket/CancelIoEx调用后不使用它(它实现已经是单独的问题(。 这是必需的,因为 Winsock客户端绝不能与另一个 Winsock 函数调用同时在套接字上发出closesocket但是我们需要随时调用closesocket以取消丢失的远程端的I/O。

从另一端看,它不能有指向OVERLAPPED(或 IT shell 类(的指针,因为套接字上可以同时有多个 I/O。 我们可以并行读写。 出于同样的原因,它也不能operation成员 - 例如,读取和写入操作可以并行处于活动状态。 所以代码像

socketState->operation = OP_READ;
socketState->thisOvl=ovl;

设计是错误的。 也没有意义hCompletion内部SocketState因为hCompletion不是每个插座。 这是错误的存储位置。

此外,我们需要强制使用不是裸OVERLAPPED结构将其传递给I/O,而是从OVERLAPPED继承的self类。 您需要在此处具有其他成员 - 指向SocketState的引用指针 - 因为当I/O完成时 - 您返回指向OVERLAPPED的指针,并且需要从中获取指向socketState的指针。operation也必须在这里(而不是SocketState(,因为操作是每个I/O但是不是每个插槽。所以在非常一般的情况下可以是下一个:

struct SocketState
{
SOCKET socket;   
HANDLE hTimer;
ULONG dwRefCount;
void AddRef();
void Release();
_NODISCARD SOCKET LockHandle();
void Rundown();
void UnlockHandle();// call closesocket on last unlock
void OnIoComplete(ULONG operation, ULONG dwErrorCode, ULONG dwBytesTransfered, PVOID buf);
void StartSomeIo(ULONG operation, PVOID buf, ULONG cb);
void Close()
{
if (LockHandle())
{
Rundown();
UnlockHandle();
}
}
};
struct UIrp : OVERLAPPED
{
SocketState* socketState;
ULONG operation;
PVOID buf;
UIrp(SocketState* socketState, ULONG operation, PVOID buf) 
: socketState(socketState), operation(operation), buf(buf)
{
RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
socketState->AddRef();
}
~UIrp()
{
socketState->Release();
}
void OnIoComplete(ULONG dwErrorCode, ULONG dwBytesTransfered)
{
socketState->OnIoComplete(operation, dwErrorCode, dwBytesTransfered, buf);
delete this;
}
};
void SocketState::StartSomeIo(ULONG operation, PVOID buf, ULONG cb)
{
if (UIrp* irp = new UIrp(this, operation, buf))
{
ULONG dwError = ERROR_INVALID_HANDLE;
if (SOCKET s = LockHandle())
{
dwError = WSA*(s,... irp, 0) == 0 ? NOERROR : WSAGetLastError();
UnlockHandle();
}
switch (dwError)
{
case NOERROR:
case WSA_IO_PENDING:
break;
default:
irp->OnIoComplete(dwError, 0);
}
}
}
void PortLoop(HANDLE hCompletionPort)
{
for (;;)
{
OVERLAPPED* lpOverlapped;
ULONG dwBytesTransfered;
ULONG_PTR CompletionKey;
ULONG dwError = GetQueuedCompletionStatus(hCompletionPort, &dwBytesTransfered, 
&CompletionKey, &lpOverlapped, INFINITE) ? NOERROR : GetLastError();
// probably somehow use CompletionKey
if (!lpOverlapped)
{
break;
}
static_cast<UIrp*>(lpOverlapped)->OnIoComplete(dwBytesTransfered, dwError);
}
}

大约

设置

在每个客户端套接字上设置TimerQueueTimer()

这是可能的,并且从设计上是正确的,但如果您有很多套接字,则认为不是最好的。 我从LIST_ENTRY继承SocketState,并将所有活动的套接字插入到某个列表中。 并通过计时器定期检查超时并关闭套接字