双工命名管道在特定写入时挂起

4
我有一个 C++ 管道服务器应用和一个 C# 管道客户端应用,通过 Windows 命名管道进行通信(双工、消息模式、在单独的读线程中等待/阻塞)。
它们之间的数据传输一切正常(通过管道发送和接收数据),直到我尝试在客户端响应表单“textchanged”事件后向管道写入数据。这时,客户端在管道写入调用(如果自动冲洗关闭,则为 flush 调用)上挂起。进入服务器应用程序时发现,它也在等待管道 ReadFile 调用而未返回。 我尝试在另一个线程上运行客户端写操作,但结果相同。
怀疑出现了某种死锁或竞争条件,但无法看到是哪里出错… 不认为我同时写入管道。
更新1:将管道从消息模式改为字节模式,结果仍然是锁定状态。
更新2:奇怪的是,只有当我从服务器向客户端积累大量数据时,才会解决锁定问题!?
服务器代码:
DWORD ReadMsg(char* aBuff, int aBuffLen, int& aBytesRead)
{
    DWORD byteCount;
    if (ReadFile(mPipe, aBuff, aBuffLen, &byteCount, NULL))
    {
        aBytesRead = (int)byteCount;
        aBuff[byteCount] = 0;
        return ERROR_SUCCESS;
    }

    return GetLastError();  
}

DWORD SendMsg(const char* aBuff, unsigned int aBuffLen)
{
    DWORD byteCount;
    if (WriteFile(mPipe, aBuff, aBuffLen, &byteCount, NULL))
    {
        return ERROR_SUCCESS;
    }

    mClientConnected = false;
    return GetLastError();  
}

DWORD CommsThread()
{
    while (1)
    {
        std::string fullPipeName = std::string("\\\\.\\pipe\\") + mPipeName;
        mPipe = CreateNamedPipeA(fullPipeName.c_str(),
                                PIPE_ACCESS_DUPLEX,
                                PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
                                PIPE_UNLIMITED_INSTANCES,
                                KTxBuffSize, // output buffer size
                                KRxBuffSize, // input buffer size
                                5000, // client time-out ms
                                NULL); // no security attribute 

        if (mPipe == INVALID_HANDLE_VALUE)
            return 1;

        mClientConnected = ConnectNamedPipe(mPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
        if (!mClientConnected)
            return 1;

        char rxBuff[KRxBuffSize+1];
        DWORD error=0;
        while (mClientConnected)
        {
            Sleep(1);

            int bytesRead = 0;
            error = ReadMsg(rxBuff, KRxBuffSize, bytesRead);
            if (error == ERROR_SUCCESS)
            {
                rxBuff[bytesRead] = 0;  // terminate string.
                if (mMsgCallback && bytesRead>0)
                    mMsgCallback(rxBuff, bytesRead, mCallbackContext);
            }
            else
            {
                mClientConnected = false;
            }
        }

        Close();
        Sleep(1000);
    }

    return 0;
}

客户端代码:

public void Start(string aPipeName)
{
    mPipeName = aPipeName;

    mPipeStream = new NamedPipeClientStream(".", mPipeName, PipeDirection.InOut, PipeOptions.None);

    Console.Write("Attempting to connect to pipe...");
    mPipeStream.Connect();
    Console.WriteLine("Connected to pipe '{0}' ({1} server instances open)", mPipeName, mPipeStream.NumberOfServerInstances);

    mPipeStream.ReadMode = PipeTransmissionMode.Message;
    mPipeWriter = new StreamWriter(mPipeStream);
    mPipeWriter.AutoFlush = true;

    mReadThread = new Thread(new ThreadStart(ReadThread));
    mReadThread.IsBackground = true;
    mReadThread.Start();

    if (mConnectionEventCallback != null)
    {
        mConnectionEventCallback(true);
    }
}

private void ReadThread()
{
    byte[] buffer = new byte[1024 * 400];

    while (true)
    {
        int len = 0;
        do
        {
            len += mPipeStream.Read(buffer, len, buffer.Length);
        } while (len>0 && !mPipeStream.IsMessageComplete);

        if (len==0)
        {
            OnPipeBroken();
            return;
        }

        if (mMessageCallback != null)
        {
            mMessageCallback(buffer, len);
        }

        Thread.Sleep(1);
    }
}

public void Write(string aMsg)
{
    try
    {
        mPipeWriter.Write(aMsg);
        mPipeWriter.Flush();
    }
    catch (Exception)
    {
        OnPipeBroken();
    }
}

你在服务器端处理读取错误的方式看起来有些不可靠。作为一种诊断措施,我建议你暂时更改服务器,使其在发生读取错误时退出,以便你可以确定客户端的写入和服务器端的读取是否与同一管道相关。 - Harry Johnston
尝试简化情况可能会有所帮助,通过更改服务器,使其不向管道写入任何数据,以便可以将客户端变为单线程。如果这不能消除问题,至少可以确定它不是某种线程问题。 - Harry Johnston
Chris: 添加了代码 :) @Harry: 它顽固地停留在ReadFile函数中,甚至没有返回错误。这可能意味着有多个管道...请看下面。尝试不写服务器:结果相同。仍在寻找... :-/ - MGB
Win32管道并不是为了实用而设计的...我也遇到了一个问题,代码必须采取大量奇怪的技巧才能正常工作。 - ActiveTrayPrntrTagDataStrDrvr
@ ActiveTrayPrntrTagDataStrDrvr 介意分享一下那些怪癖吗? - Pierre
3个回答

7
如果您使用单独的线程,则无法同时从管道中读取和写入。例如,如果您正在从管道中进行阻塞读取,然后进行后续的阻塞写入(来自不同的线程),则写入调用将等待/阻塞,直到读取调用完成,并且在许多情况下,如果这是意外行为,您的程序将死锁。
我尚未测试重叠I/O,但它可能能够解决此问题。但是,如果您决定使用同步调用,则以下模型可能会帮助您解决问题。
主/从
您可以实现主/从模型,在该模型中,客户端或服务器是主机,另一端仅响应,这通常是您将在MSDN示例中找到的内容。
在某些情况下,如果从机定期需要向主机发送数据,则可能会发现此方法有问题。您必须使用外部信号机制(在管道之外)或者使主机定期查询/轮询从机,或者交换角色,其中客户端是主机,服务器是从机。
写入器/读取器
您可以使用写入器/读取器模型,其中使用两个不同的管道。但是,如果有多个客户端,则必须以某种方式关联这两个管道,因为每个管道都将具有不同的句柄。您可以通过在连接到每个管道时由客户端发送唯一的标识符值来执行此操作,这将使服务器关联两个管道。此数字可以是当前系统时间或全局或本地的唯一标识符。
线程
如果您决定使用同步API,则可以在主/从模型中使用线程,如果您不想在等待从机端的消息时被阻止。但是,您将要锁定读取器,在它读取消息(或遇到一系列消息的结尾)之后写出响应(作为从机),最后解锁读取器。您可以使用将线程置于睡眠状态的锁定机制来锁定和解锁读取器,因为这些机制最有效。
TCP的安全问题
与命名管道相比,使用TCP的损失也是最大的潜在问题。TCP流不包含任何安全性。因此,如果安全性是一个问题,则必须实现它,并且您有可能创建安全漏洞,因为您必须自己处理身份验证。如果正确设置参数,则命名管道可以提供安全性。还要再次明确指出:安全性并不简单,通常您会希望使用已经设计为提供安全性的现有设施。

0

我认为你可能遇到了命名管道消息模式的问题。在此模式下,对内核管道句柄的每次写入都构成一条消息。这并不一定与您的应用程序所认为的消息相对应,而且消息可能比您的读取缓冲区大。

这意味着您的管道读取代码需要两个循环,内部循环读取直到当前[命名管道]消息完全接收,外部循环读取直到您的[应用程序级别]消息接收完毕。

您的C#客户端代码确实有一个正确的内部循环,如果IsMessageComplete为false,则再次读取:

do
{
    len += mPipeStream.Read(buffer, len, buffer.Length);
} while (len>0 && !mPipeStream.IsMessageComplete);

你的C++服务器代码没有这样的循环 - 在Win32 API级别上等效的是检测返回码ERROR_MORE_DATA。
我猜想这可能导致客户端在一个管道实例上等待服务器读取,而服务器在另一个管道实例上等待客户端写入。

我懂你的意思:
  • 我尝试了一个非常大的缓冲区(400k!),但我只发送了一个40个字符的字符串。
  • 服务端在读取函数之前和之后等待客户端发送的消息 - 它没有任何反应。
  • 同意这可能是两个管道实例的问题,但之前的通信正常,也没有遇到任何断点来关闭/重新打开管道。
- MGB
尝试使用字节模式的管道,还尝试将客户端发送延迟到更新循环中。但仍然存在相同的问题。 - MGB

0

我觉得你尝试做的事情可能不会按预期工作。

有一段时间,我也在尝试做类似于你的代码的东西,并获得了类似的结果,管道卡住了, 很难确定出了什么问题。

我建议以非常简单的方式使用客户端:

  1. 创建文件
  2. 编写请求
  3. 读取答案
  4. 关闭管道。

如果您想与客户端进行双向通信,并且还能够从服务器接收未经请求的数据,则应该 实现两个服务器。这是我使用的解决方法:在这里您可以找到源代码


谢谢回复。实际上,我已经将其更改为使用TCP而不是命名管道,这样就可以正常工作了。 - MGB

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