与浏览器通信时奇怪的 IOCP 行为
Strange IOCP behaviour when communicating with browsers
我正在为从桌面客户端到浏览器的视频流编写 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 returns TRUE 并将设置转移到 0 - 在本例中为 WSAGetLastError() returns 64(指定的网络名称不再可用),并且有意义 - 客户端已断开连接(符合 if(!QCS || (QCS && !transfered))
)。但是当浏览器断开连接时,错误代码让我感到困惑......它可以是0、997(等待操作)、87(无效参数)......并且没有与连接结束相关的代码。
为什么 IOCP select 这个事件?它怎么能select一个挂起的操作呢?为什么传输 0 个字节时错误为 0?它还会导致无休止地尝试删除与重叠结构关联的对象,因为析构函数调用 ~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); }
进行安全删除。在 DeleteClient
调用中,套接字以 closesocket()
关闭,但是,如您所见,我在它之前发布了一个 shutdown(client->Socket(), SD_SEND);
调用(在 FIN_CLOSE
部分)。
我知道连接有两端,在服务器端关闭它并不意味着另一端也会关闭它。但我需要创建一个稳定的服务器,不受坏连接和半开连接的影响。例如,Web 应用程序的用户可以快速按 F5 几次重新加载页面(是的,有些人这样做 :))- 连接将重新打开几次,并且服务器不能由于此操作而延迟或崩溃。
如何在 IOCP 中处理这个 "bad" 事件?
你这里有很多错误代码。
while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);
这不是有效的错误停止代码 WorkerThread
。起初你做多余的调用 WaitForSingleObject
,使用多余的 EventShutdown
和 main 这无论如何都无法关闭。如果您的代码等待 GetQueuedCompletionStatus
内的数据包,您说 EventShutdown
- 不中断 GetQueuedCompletionStatus
调用 - 您将继续无限等待。关闭的正确方法 - PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
而不是调用 SetEvent(EventShutdown)
并且如果工作线程视图 client == 0
- 他打破循环。通常你需要多个 WorkerThread
(不是单个)。和多次调用 PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
- 工作线程的精确计数。您还需要将此调用与 io 同步 - 只有在所有 io 已经完成并且没有新的 io 数据包将排队到 iocp 之后才执行此操作。所以 "null packets" 必须是最后一个排队到端口
if(!QCS || (QCS && !transfered)){
printf("Error %d\n", WSAGetLastError());
DeleteClient(client);
continue;
}
if !QCS
- client
中的值未初始化,您根本无法使用它并且在这种情况下调用 DeleteClient(client);
是错误的
当对象 (client
) 从多个线程使用时 - 谁必须删除它?如果一个线程删除对象,而另一个线程仍在使用它会怎样?正确的解决方案是在此类对象(客户端)上使用引用计数。并根据您的代码 - 每个 hIOCP 都有一个客户端?因为您检索客户端的指针作为 hIOCP 的完成键,它对于绑定到 hIOCP 的套接字上的所有 I/O 操作都是单一的。这一切都是错误的设计。
您需要在 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
?如果是,这是致命错误。对于每个 I/O 操作,IO_Context
必须是唯一的。
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
的定义可能会中断。如果您使用 struct IO_Context{
OVERLAPPED overlapped; }
,则可以使用 CONTAINING_RECORD(overlapped, IO_Context, overlapped)
,但最好使用 class IO_Context : public OVERLAPPED
和 static_cast<IO_Context*>(overlapped)
现在关于 为什么 IOCP select 这个事件?如何在 IOCP 中处理这个 "bad" 事件?
IOCP 无select。他只是在 I/O 完成时发出信号。全部。您在不同的网络操作中遇到的特定 wsa 错误完全独立于使用 IOCP 或任何其他完成机制。
当错误代码为 0 并且在 recv 操作中传输了 0 个字节时,正常断开连接是正常的。您需要在连接完成后永久激活 recv 请求,如果 recv 完成并传输了 0 个字节,这意味着断开连接发生
我正在为从桌面客户端到浏览器的视频流编写 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 returns TRUE 并将设置转移到 0 - 在本例中为 WSAGetLastError() returns 64(指定的网络名称不再可用),并且有意义 - 客户端已断开连接(符合 if(!QCS || (QCS && !transfered))
)。但是当浏览器断开连接时,错误代码让我感到困惑......它可以是0、997(等待操作)、87(无效参数)......并且没有与连接结束相关的代码。
为什么 IOCP select 这个事件?它怎么能select一个挂起的操作呢?为什么传输 0 个字节时错误为 0?它还会导致无休止地尝试删除与重叠结构关联的对象,因为析构函数调用 ~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); }
进行安全删除。在 DeleteClient
调用中,套接字以 closesocket()
关闭,但是,如您所见,我在它之前发布了一个 shutdown(client->Socket(), SD_SEND);
调用(在 FIN_CLOSE
部分)。
我知道连接有两端,在服务器端关闭它并不意味着另一端也会关闭它。但我需要创建一个稳定的服务器,不受坏连接和半开连接的影响。例如,Web 应用程序的用户可以快速按 F5 几次重新加载页面(是的,有些人这样做 :))- 连接将重新打开几次,并且服务器不能由于此操作而延迟或崩溃。
如何在 IOCP 中处理这个 "bad" 事件?
你这里有很多错误代码。
while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);
这不是有效的错误停止代码 WorkerThread
。起初你做多余的调用 WaitForSingleObject
,使用多余的 EventShutdown
和 main 这无论如何都无法关闭。如果您的代码等待 GetQueuedCompletionStatus
内的数据包,您说 EventShutdown
- 不中断 GetQueuedCompletionStatus
调用 - 您将继续无限等待。关闭的正确方法 - PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
而不是调用 SetEvent(EventShutdown)
并且如果工作线程视图 client == 0
- 他打破循环。通常你需要多个 WorkerThread
(不是单个)。和多次调用 PostQueuedCompletionStatus(hIOCP, 0, 0, 0)
- 工作线程的精确计数。您还需要将此调用与 io 同步 - 只有在所有 io 已经完成并且没有新的 io 数据包将排队到 iocp 之后才执行此操作。所以 "null packets" 必须是最后一个排队到端口
if(!QCS || (QCS && !transfered)){
printf("Error %d\n", WSAGetLastError());
DeleteClient(client);
continue;
}
if !QCS
- client
中的值未初始化,您根本无法使用它并且在这种情况下调用 DeleteClient(client);
是错误的
当对象 (client
) 从多个线程使用时 - 谁必须删除它?如果一个线程删除对象,而另一个线程仍在使用它会怎样?正确的解决方案是在此类对象(客户端)上使用引用计数。并根据您的代码 - 每个 hIOCP 都有一个客户端?因为您检索客户端的指针作为 hIOCP 的完成键,它对于绑定到 hIOCP 的套接字上的所有 I/O 操作都是单一的。这一切都是错误的设计。
您需要在 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
?如果是,这是致命错误。对于每个 I/O 操作,IO_Context
必须是唯一的。
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
的定义可能会中断。如果您使用 struct IO_Context{
OVERLAPPED overlapped; }
,则可以使用 CONTAINING_RECORD(overlapped, IO_Context, overlapped)
,但最好使用 class IO_Context : public OVERLAPPED
和 static_cast<IO_Context*>(overlapped)
现在关于 为什么 IOCP select 这个事件?如何在 IOCP 中处理这个 "bad" 事件?
IOCP 无select。他只是在 I/O 完成时发出信号。全部。您在不同的网络操作中遇到的特定 wsa 错误完全独立于使用 IOCP 或任何其他完成机制。
当错误代码为 0 并且在 recv 操作中传输了 0 个字节时,正常断开连接是正常的。您需要在连接完成后永久激活 recv 请求,如果 recv 完成并传输了 0 个字节,这意味着断开连接发生