我正在编写IOCP服务器,用于从桌面客户端到浏览器的视频流传输。 双方都使用WebSocket协议来统一服务器的体系结构(并且因为浏览器没有其他方法可以执行全双工交换)。
工作线程开始如下:
unsigned int __stdcall WorkerThread(void * param){
int ThreadId = (int)param;
OVERLAPPED *overlapped = nullptr;
IO_Context *ctx = nullptr;
Client *client = nullptr;
DWORD transfered = 0;
BOOL QCS = 0;
while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);
if(!client){
if( Debug ) printf("No client\n");
break;
}
ctx = (IO_Context *)overlapped;
if(!QCS || (QCS && !transfered)){
printf("Error %d\n", WSAGetLastError());
DeleteClient(client);
continue;
}
switch(auto opcode = client->ProcessCurrentEvent(ctx, transfered)){
// Client owed to receive some data
case OPCODE_RECV_DEBT:{
if((SOCKET_ERROR == client->Recv()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
break;
}
// Client received all data or the beginning of new message
case OPCODE_RECV_DONE:{
std::string message;
client->GetInput(message);
// Analizing the first byte of WebSocket frame
switch( opcode = message[0] & 0xFF ){
// HTTP_HANDSHAKE is 'G' - from GET HTTP...
case HTTP_HANDSHAKE:{
message = websocket::handshake(message);
while(!client->SetSend(message)) Sleep(1); // Set outgoing data
if((SOCKET_ERROR == client->Send()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
break;
}
// Browser sent a closing frame (0x88) - performing clean WebSocket closure
case FIN_CLOSE:{
websocket::frame frame;
frame.parse(message);
frame.masked = false;
if( frame.pl_len == 0 ){
unsigned short reason = 1000;
frame.payload.resize(sizeof(reason));
frame.payload[0] = (reason >> 8) & 0xFF;
frame.payload[1] = reason & 0xFF;
}
frame.pack(message);
while(!client->SetSend(message)) Sleep(1);
if((SOCKET_ERROR == client->Send()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
shutdown(client->Socket(), SD_SEND);
break;
}
IO上下文结构:
struct IO_Context{
OVERLAPPED overlapped;
WSABUF data;
char buffer[IO_BUFFER_LENGTH];
unsigned char opcode;
unsigned long long debt;
std::string message;
IO_Context(){
debt = 0;
opcode = 0;
data.buf = buffer;
data.len = IO_BUFFER_LENGTH;
overlapped.Offset = overlapped.OffsetHigh = 0;
overlapped.Internal = overlapped.InternalHigh = 0;
overlapped.Pointer = nullptr;
overlapped.hEvent = nullptr;
}
~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); }
};
客户端发送功能:
int Client::Send(){
int var_buf = O.message.size();
// "O" is IO_Context for Output
O.data.len = (var_buf>IO_BUFFER_LENGTH)?IO_BUFFER_LENGTH:var_buf;
var_buf = O.data.len;
while(var_buf > 0) O.data.buf[var_buf] = O.message[--var_buf];
O.message.erase(0, O.data.len);
return WSASend(connection, &O.data, 1, nullptr, 0, &O.overlapped, nullptr);
}
当桌面客户端断开连接时(它仅使用closesocket()来执行此操作,而没有shutdown()),GetQueuedCompletionStatus返回TRUE,并将设置转移为0-在这种情况下,WSAGetLastError()返回64(指定的网络名称不再是可用),并且有意义-客户端已断开连接(与if(!QCS || (QCS && !transfered))
对齐)。但是当浏览器断开连接时,错误代码使我感到困惑...它可能是0、997(待处理操作),87(无效参数)...并且没有与连接终止有关的代码。
IOCP为什么选择此事件?如何选择待处理的操作?为什么传输0字节时错误为0?另外,由于析构函数调用~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); }
进行安全删除,因此它也导致了尝试删除与重叠结构关联的对象的无尽尝试。在DeleteClient
调用中,套接字以closesocket()
关闭,但是,正如您所看到的,我在它之前发布了一个shutdown(client->Socket(), SD_SEND);
调用(在FIN_CLOSE
部分中)。>
我知道连接有两个方面,而在服务器端将其关闭并不意味着另一端也会将其关闭。但是我需要创建一个稳定的服务器,避免出现不良连接和半开连接。例如,Web应用程序的用户可以快速按F5几次以重新加载页面(是的,有些家伙这样做:))-连接将重新打开几次,并且服务器不得由于此操作而滞后或崩溃。 >
如何处理IOCP中的“不良”事件?
答案 0 :(得分:1)
您在这里有很多错误的代码。
while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);
这不是高效的,并且停止WorkerThread
的代码错误。首先,您进行多余的呼叫WaitForSingleObject
,使用多余的EventShutdown
并保持这一点,但无论如何都无法关机。如果您的代码在GetQueuedCompletionStatus
中等待您说EventShutdown
内的数据包-不中断GetQueuedCompletionStatus
调用-您将在此处继续无限等待。正确的关机方式-PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
代替调用SetEvent(EventShutdown)
,如果工作线程视图client == 0
-他中断循环。通常您需要多个WorkerThread
(不是单个)。以及多次调用PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
-工作线程的确切计数。另外,您还需要将此调用与io同步-仅在所有io已经完成并且没有新的io数据包排队到iocp之后再执行此操作。因此“空数据包”必须是最后一个排队到端口的数据
if(!QCS || (QCS && !transfered)){
printf("Error %d\n", WSAGetLastError());
DeleteClient(client);
continue;
}
如果!QCS
-client
中的值未初始化,则无法使用它,并在这种情况下调用DeleteClient(client);
是错误的
何时在多个线程中使用对象(client
)-谁必须删除它?如果一个线程删除对象,而另一个仍使用它,该怎么办?正确的解决方案是,如果您对此类对象(客户端)使用引用计数。并根据您的代码-每个hIOCP有一个客户端?因为您将客户端的检索器指针作为hIOCP的完成密钥,该套接字对于套接字上的所有I / O操作都是单一的,因此绑定到hIOCP。这一切都是错误的设计。
您需要在IO_Context
中存储指向客户端的指针。并在IO_Context
中添加对客户端的引用,并在IO_Context
析构函数中释放客户端。
class IO_Context : public OVERLAPPED {
Client *client;
ULONG opcode;
// ...
public:
IO_Context(Client *client, ULONG opcode) : client(client), opcode(opcode) {
client->AddRef();
}
~IO_Context() {
client->Release();
}
void OnIoComplete(ULONG transfered) {
OnIoComplete(RtlNtStatusToDosError(Internal), transfered);
}
void OnIoComplete(ULONG error, ULONG transfered) {
client->OnIoComplete(opcode, error, transfered);
delete this;
}
void CheckIoError(ULONG error) {
switch(error) {
case NOERROR:
case ERROR_IO_PENDING:
break;
default:
OnIoComplete(error, 0);
}
}
};
那么您有一个IO_Context
吗?如果是,这是致命错误。 IO_Context
对于每个I / O操作必须唯一。
if (IO_Context* ctx = new IO_Context(client, op))
{
ctx->CheckIoError(WSAxxx(ctx) == 0 ? NOERROR : WSAGetLastError());
}
并从工作线程 s
ULONG WINAPI WorkerThread(void * param)
{
ULONG_PTR key;
OVERLAPPED *overlapped;
ULONG transfered;
while(GetQueuedCompletionStatus(hIOCP, &transfered, &key, &overlapped, INFINITE)) {
switch (key){
case '_io_':
static_cast<IO_Context*>(overlapped)->OnIoComplete(transfered);
continue;
case 'stop':
// ...
return 0;
default: __debugbreak();
}
}
__debugbreak();
return GetLastError();
}
像while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1);
这样的代码总是错误的。绝对和永远。永远不要写这样的代码。
ctx = (IO_Context *)overlapped;
尽管在您的具体情况下会给出正确的结果,但效果不佳,并且如果您更改IO_Context
的定义可能会中断。如果使用CONTAINING_RECORD(overlapped, IO_Context, overlapped)
,则可以使用struct IO_Context{
OVERLAPPED overlapped; }
,但最好使用class IO_Context : public OVERLAPPED
和static_cast<IO_Context*>(overlapped)
关于为什么IOCP为什么选择此事件?如何处理IOCP中的“不良”事件?
IOCP 无选择。他只是在I / O完成时发出信号。所有。绝对独立于使用IOCP或任何其他完成机制的情况,您在不同的网络操作上遇到哪些特定的wsa错误。
当错误代码为0并且在recv操作中传输了0个字节时,正常断开时的是正常的。您需要在连接完成后永久激活recv请求,如果recv完成并传输了0个字节,则表示断开连接