异步、非阻塞套接字行为 - WSAEWOULDBLOCK

5
我继承了两个应用程序,一个测试工具(Test Harness)作为客户端运行在Windows 7 PC上,另一个服务器应用程序运行在Windows 10 PC上。我试图使用TCP/IP套接字(Socket)在这两个应用程序之间进行通信。客户端发送请求(以XML格式的数据)到服务器,服务器再将所请求的数据(同样是XML)发送回客户端。
如下所示是设置过程:
       Client                                    Server
--------------------                      --------------------  
|                  |    Sends Requests    |                  |
|   Client Socket  |  ----------------->  |   Server Socket  |
|                  |  <-----------------  |                  |
|                  |      Sends Data      |                  |
--------------------                      --------------------

这个过程总是在初始连接时起作用(即新启动的客户端和服务器应用程序)。 客户端可以断开与服务器的连接,这会触发套接字的清理。 重新连接后,我几乎总是(虽然不总是)收到以下错误:

"Receive() - The socket is marked as nonblocking and the receive operation would block"

此错误显示在客户端,并且涉及的套接字是异步、非阻塞套接字。

导致此 SOCKET_ERROR 的行是:

numBytesReceived = theSocket->Receive(theReceiveBuffer, 10000));

where:
- numBytesReceived is an integer (int)
- theSocket is a pointer to a class called CClientSocket which is a specialisation of CASyncSocket, which is part of the MFC C++ Library.  This defines the socket object which is embedded within the client.  It is an asynchonous, non-blocking socket.
- Receive() is a virtual function within the CASyncSocket object
- theReceiveBuffer is a char array (10000 elements)

在执行上述代码时,该函数返回SOCKET_ERROR,并调用theSocket->GetLastError()返回WSAEWOULDBLOCK

SocketTools指出:

当非阻塞(异步)套接字尝试执行无法立即执行的操作时,将返回错误10035。此错误不是致命错误,应由应用程序视为建议性错误。此错误代码对应于Windows套接字错误WSAEWOULDBLOCK。

从非阻塞套接字读取数据时,如果此时没有更多可读取的数据,则会返回此错误。在这种情况下,应用程序应等待OnRead事件触发,表示已有更多数据可供读取。可以使用IsReadable属性确定是否可以从套接字读取数据。

向非阻塞套接字写入数据时,如果本地套接字缓冲区在等待远程主机读取一些数据时被填满,则会返回此错误。当缓冲区空间变得可用时,OnWrite事件将触发,表示可以写入更多数据。可以使用IsWritable属性确定是否可以向套接字写入数据。

需要注意的是,应用程序将不知道可以在单个写操作中发送多少数据,因此,如果客户端尝试过快地发送过多数据,则可能会多次返回此错误。如果在发送数据时频繁出现此错误,可能表示网络延迟较高或远程主机无法快速读取数据。

一直收到此错误并未能在套接字上接收任何内容。

使用Wireshark,以下通信与源、目的地和TCP位标志发生:

事件:通过TCP/IP连接测试工具到服务器

Client --> Server: SYN
Server --> Client: SYN, ACK
Client --> Server: ACK

This appears to be correct and represents the Three-Way Handshake of connecting.

SocketSniff confirms that a Socket is closed on the client side.  It was not possible to get SocketSniff to work with the Windows 10 Server application.

事件:从测试工具发送请求获取数据。
Client --> Server: PSH, ACK
Server --> Client: PSH, ACK
Client --> Server: ACK

Both request data and received data is confirmed to be exchanged successfully

事件:从服务器断开测试工具

Client --> Server: FIN, ACK
Server --> Client: ACK
Server --> Client: FIN, ACK
Client --> Server: ACK

This appears to be correct and represents the Four-Way handshake of connection closure.

SocketSniff confirms that a Socket is closed on the client side.  It was not possible to get SocketSniff to work with the Windows 10 Server application.

事件:通过TCP/IP重新连接测试工具到服务器

Client --> Server: SYN
Server --> Client: SYN, ACK
Client --> Server: ACK

This appears to be correct and represents the Three-Way Handshake of connecting.

SocketSniff confirms that a new Socket is opened on the client side.  It was not possible to get SocketSniff to work with the Windows 10 Server application.

事件:向测试工具发送数据请求
Client --> Server: PSH, ACK
Server --> Client: ACK

We see no data being pushed (PSH) back to the client, yet we do see an acknowledgement.  

有没有人知道这里可能出了什么问题?我知道你们看不到源代码很难进行诊断,但我希望其他人能分享一下遇到这个错误的经验,并指出具体的调查路线。

更多信息:

服务器初始化一个监听线程并绑定到0.0.0.0:49720。'WSAStartup()', 'bind()'和'listen()'函数都返回'0',表示成功。该线程在服务器应用程序的整个生命周期中持续存在。

服务器初始化了两个线程,一个读线程和一个写线程。读线程负责从其套接字读取请求数据,并使用名为Connection的类进行初始化:

HANDLE theConnectionReadThread 
           = CreateThread(NULL,                                    // Security Attributes
                          0,                                       // Default Stacksize
                          Connection::connectionReadThreadHandler, // Callback
                          (LPVOID)this,                            // Parameter to pass to thread
                          CREATE_SUSPENDED,                        // Don't start yet
                          NULL);                                   // Don't Save Thread ID
             

编写线程的初始化方式相似。

在每种情况下,CreateThread()函数返回一个合适的HANDLE,例如:

theConnectionReadThread  = 00000570
theConnectionWriteThread = 00000574  

实际上,这些线程是在以下函数内部启动的:
void Connection::startThreads()
{
    ResumeThread(theConnectionReadThread);
    ResumeThread(theConnectionWriteThread);
}                                   

这个函数是从另一个名为ConnectionManager的类中调用的,该类管理与服务器的所有可能连接。在这种情况下,我只关心单个连接,以保持简单。
将文本输出添加到服务器应用程序后,可以发现在观察到错误行为之前,可以成功地连接/断开连接客户端和服务器多次。例如,在connectionReadThreadHandler()connectionWriteThreadHandler()函数中,它们执行时立即向日志文件输出文本。
当观察到正确的行为时,以下行会输出到日志文件:
Connection::ResumeThread(theConnectionReadThread) returned 1
Connection::ResumeThread(theConnectionWriteThread) returned 1
ConnectionReadThreadHandler() Beginning
ConnectionWriteThreadHandler() Beginning

如果观察到错误行为,则会将以下行输出到日志文件:

Connection::ResumeThread(theConnectionReadThread) returned 1
Connection::ResumeThread(theConnectionWriteThread) returned 1

回调函数似乎没有被调用。
此时客户端会显示错误,提示:
"Receive() - The socket is marked as nonblocking and the receive operation would block"

在客户端,我有一个名为CClientDoc的类,其中包含客户端套接字代码。它首先初始化theSocket,它是嵌入在客户机中的套接字对象:
private:
    CClientSocket* theSocket = new CClientSocket;

当客户端和服务器之间初始化连接时,该类会调用一个名为CreateSocket()的函数,下面是它的一部分代码,以及它调用的辅助函数:

void CClientDoc::CreateSocket()
{
    AfxSocketInit();
    int lastError;
    theSocket->Init(this);
    
    if (theSocket->Create()) // Calls CAyncSocket::Create() (part of afxsock.h)
    {
        theErrorMessage = "Socket Creation Successful"; // this is a CString
        theSocket->SetSocketStatus(WAITING);             
    }
    else
    {
        // We don't fall in here
    }
}

void CClientDoc::Init(CClientDoc* pDoc)
{
    pClient = pDoc; // pClient is a pointer to a CClientDoc
}

void CClientDoc::SetSocketStatus(SOCKET_STATUS sock_stat)
{
    theSocketStatus = sock_stat; // theSocketStatus is a private member of CClientSocket of type SOCKET_STATUS
}

CreateSocket()之后立即调用SetupSocket(),该函数如下:

void CClientDoc::SetupSocket()
{
    theSocket->AsyncSelect(); // Function within afxsock.h
}

当客户端与服务器断开连接时,

void CClientDoc::OnClienDisconnect()
{
    theSocket->ShutDown(2); // Inline function within afxsock.inl
    delete theSocket;
    theSocket = new CClientSocket;
    CreateSocket();
    SetupSocket();        
}

因此,我们删除当前的套接字,然后创建一个新的套接字,准备好使用,这似乎按预期工作。

错误信息被写在DoReceive()函数中的客户端上。该函数调用套接字尝试读取消息。

CClientDoc::DoReceive()
{
    int lastError;
    switch (numBytesReceived = theSocket->Receive(theReceiveBuffer, 10000))
    {
    case 0:
        // We don't fall in here
        break;
    case SOCKET_ERROR: // We come in here when the faulty behaviour occurs
        if (lastError = theSocket->GetLastError() == WSAEWOULDBLOCK)
        {
            theErrorMessage = "Receive() - The socket is marked as nonblocking and the receive operation would block";
        }
        else
        {
            // We don't fall in here
        }
        break;
    default:
        // When connection works, we come in here
        break;
    }
}

希望这些代码的添加能够提供一些启示。如果需要,我可以再添加一些代码。
谢谢。

只是想提一下:描述得很好。 - koviroli
你说你“一直”收到这个错误。那么你是等待并重试吗?你确定错误出现在接收调用上吗?客户端是否可能没有发送完整的请求?你能否使用ncat模拟客户端发送的相同请求,并查看服务器的响应? - ewindes
是的,我等一会儿再试一次,套接字似乎仍然处于相同的阻塞状态。它似乎会一直保持这种状态,直到我通过重新启动两个应用程序来刷新服务器和客户端。关于接收调用是否存在错误,这是一个很好的观点。我将尝试使用“ncat”并更新我的结果。 - user11189672
关于@ewindes的问题,使用ncat时,问题无法重现。一切似乎都按预期运行。 - user11189672
1个回答

1
WSAEWOULDBLOCK错误并不意味着套接字被标记为阻塞状态。它意味着套接字被标记为非阻塞状态,并且此时没有数据可供读取。 WSAEWOULDBLOCK意味着如果套接字被标记为阻塞状态,那么调用线程将被阻塞等待数据。
要知道非阻塞套接字何时有数据可供读取,请使用Winsock的select()函数,或CClientSocket::AsyncSelect()方法请求FD_READ通知,或其他等效方法。在有数据可读之前不要尝试读取。
在你的分析中,你发现客户端向服务器发送数据,但服务器没有向客户端发送数据。因此,你的代码明显存在逻辑错误,需要找到并修复它。可能是客户端没有正确终止请求,或者服务器没有正确接收/处理/回复请求。但由于你没有展示实际的代码,我们无法告诉你具体哪里出了问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接