• Программирование
  • C++
  • Веб-сервисы, защищенные посредством промежуточного программного обеспечения, ориентированного на обработку сообщений

Использование почтовых ящиков для связи между процессами - Классы CMailslot, CSyncMailslotWriter и CSyncMailslotReader

ОГЛАВЛЕНИЕ

CMailslot

CMailslot выглядит так:

// Базовый класс почтового ящика, содержит элементы, общие для серверов и клиентов
class CMailslot
{
protected:
                    CMailslot();
    virtual         ~CMailslot();

    virtual bool    Connect(LPCTSTR szSlotName,
                            LPCTSTR szServerName = _T(".")) = 0;

public:
    virtual void    Disconnect();

    bool            IsOpen() const
                    { return m_hMailSlot != INVALID_HANDLE_VALUE; }

protected:
    HANDLE          m_hMailSlot,
                    m_hStopEvent;
    bool            m_bStop;
    LPTSTR          m_pszSlotname;
    COverlappedIO   m_overlapped;
};

Класс является абстрактным базовым классом, поэтому нельзя прямо создать экземпляр класса CMailslot. Дескриптор m_hMaiSlot вполне понятен, но остальные члены данных не столь очевидны. Вкратце, почти все остальные переменные члена существуют для обеспечения постепенного отключения многопоточного приложения (смотрите подробности в этой статье и читайте здесь сведения о прерывании взаимоисключающих вызовов). Класс COverlappedIO применен для инкапсуляции логики совмещенного ввода-вывода.

Заметьте, что метод Connect() является виртуальным и абстрактным. Это так, потому что, как сказано выше, способ открытия почтового ящика зависит от того, являетесь ли вы читателем или писателем. Класс виртуальный, чтобы его можно было переопределять, и абстрактный, чтобы заставить каждый производный класс действительно реализовывать функцию. Предполагается, что класс читателя, производный CMaislot, использует API CreateMailSlot() для открытия почтового ящика; класс писателя использует API CreateFile()для открытия почтового ящика. (Ни в том, ни) И в том, и  в другом случае есть разумное стандартное поведение. Метод Connect() требует имя почтового ящика и принимает необязательное имя сервера, принимает значение по умолчанию '.' (точка), которая, как уже сказано, обозначает локальный компьютер.

Disconnect() отличается, поскольку разумное стандартное поведение только требует, чтобы дескриптор почтового ящика закрывался. Могут требоваться другие типы поведения, но так как закрытие дескриптора является разумным минимумом, класс не требует большего.

IsOpen() возвращает bool, показывающий, был ли почтовый ящик успешно открыт. Функция просто проверяет, что член m_hMailSlot не является INVALID_HANDLE_VALUE. Как станет ясно позже, это не очень качественная проверка, но лучшая, которую можно провести при попытке записи в почтовый ящик.

CSyncMailslotWriter

Этот класс реализует писателя почтового ящика. Он выглядит так.

// Класс писателя почтового ящика. Используется для записи в почтовый ящик.
// Класс создает асинхронный дескриптор почтового ящика, используемый
// при совмещенном вводе-выводе для записи сообщений, помещаемых в очередь.
class CSyncMailslotWriter : public CMailslot
{
public:
                    CSyncMailslotWriter();
    virtual         ~CSyncMailslotWriter();

    virtual bool    Connect(LPCTSTR szSlot, LPCTSTR szServer = _T("."));

    virtual DWORD   Write(BYTE *pbData, DWORD dwDataLength);

protected:
    virtual bool    Connect();
};

Класс предоставляет требуемое переопределение открытого метода Connect(), выглядящее так.

//    Создает подключение к почтовому ящику.
//    Возвращает истину при успехе, ложь - при неудаче.
bool CSyncMailslotWriter::Connect(LPCTSTR szSlotname, LPCTSTR szServer)
{
    assert(szServer);
    assert(szSlotname);

    //    Удаляет любое прежнее имя почтового ящика
    delete m_pszSlotname;
    m_pszSlotname = new TCHAR[_MAX_PATH];
    assert(m_pszSlotname);

    //    Создает новое имя почтового ящика
    _sntprintf(m_pszSlotname, _MAX_PATH, _T(\\\\%s\\mailslot\\%s),
                   szServer, szSlotname);
    m_pszSlotname[_MAX_PATH - sizeof(TCHAR)] = TCHAR(0);
   
    //    Подключается...
    return Connect();
}

Это весьма просто. Сначала проверяется правильность входных параметров. Затем создается каноническая форма имени почтового ящика и вызывается закрытый метод Connect(), осуществляющий фактическое подключение к почтовому ящику.

Мы сделали это так, потому что хотели, чтобы классы почтового ящика справлялись с ошибками сети, не требуя, чтобы клиент слишком много знал об обработке ошибок. Читатель, подключенный к сети или нет, может внезапно исчезнуть. Когда это происходит, писатель должен пытаться восстановить соединение, но если это не удается, он не должен блокировать клиента. В последнем случае, если класс не может восстановить соединение, он отбрасывает сообщение. При такой модели информация может теряться, но хотя бы клиент продолжает работать. Клиент имеет право делать то, что выберет, когда получает false(ложь) в качестве возвращаемого значения из метода Write(). Он может повторить Write() или проигнорировать ошибку. Итак, есть два метода Connect(). Один открытый, принимающий параметры имя сервера и почтового ящика, а второй закрытый, выполняющий фактическое подключение. Клиент вызывает открытый метод, и не знает и не интересуется тем, что есть закрытый метод, осуществляющий фактическое подключение. Закрытый метод выглядит так:

bool CSyncMailslotWriter::Connect()
{
    //  Закрывает любой имеющийся почтовый ящик
    Disconnect();

    //  Открывает почтовый ящик для совмещенного ввода-вывода
    if ((m_hMailSlot = CreateFile(m_pszSlotname,
                            GENERIC_WRITE,
                            FILE_SHARE_READ | FILE_SHARE_WRITE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
                            &m_overlapped)
        ) != INVALID_HANDLE_VALUE)
    {
        m_overlapped.Attach(m_hMailSlot);
        return true;
    }

    return false;
}

Этот метод отключается от любого прежнего почтового ящика и затем открывает подключение к почтовому ящику с помощью API CreateFile(). Если он преуспевает, то прикрепляет новый дескриптор почтового ящика к объекту COverlappedIO и возвращает true, иначе он возвращает false.

Метод Write() выглядит так:

//  Записывает сообщение в почтовый ящик.
DWORD CSyncMailslotWriter::Write(BYTE *pbData, DWORD dwDataLength)
{
    assert(pbData);
    assert(dwDataLength);

    int nRetries = 2;

    while (nRetries--)
    {
        //  Если почтовый ящик закрыт, пытается вновь подключиться к нему
        if (!IsOpen() && m_pszSlotname != LPTSTR(NULL))
            Connect();

        DWORD dwWrittenLength = 0;

        if (IsOpen())
        {
            //  Пишет с помощью совмещенного ввода-вывода. Совмещенный ввод-вывод
            //  используется, чтобы иметь возможность прервать запись. Если
            //  используется синхронный ввод-вывод, есть вероятность, что работа
            //  остановится внутри вызова WriteFile. Более подробные объяснения
            //  смотрите в
            //  http://www.codeproject.com/win32/overlappedio.asp
            if (m_overlapped.Write(pbData, dwDataLength, &dwWrittenLength,
                                   m_hStopEvent)
                             && dwWrittenLength == dwDataLength)
                //  Ввод-вывод завершился, поэтому возвращается успех (истина).
                return dwWrittenLength;
            else
                //  Если запись не удалась, она отвергается и производится
                //  отключение, чтобы следующая запись попыталось подключиться.
                Disconnect();
        }
    }

    return 0;
}

Этот метод делает две попытки записать сообщение в почтовый ящик. Если почтовый ящик открыт и запись удается – он возвращает количество записанных байтов. Если запись срывается - он выполняет Disconnect(), обходит цикл и повторяет попытку. Если попытка Connect() удается - он записывает сообщение и возвращает количество записанных байтов. Если Connect() не удается - метод возвращает 0 в качестве количества записанных байтов, и вызывающая функция решает, что делать с сообщением.

Вероятно, вы заметили, что класс является синхронным, но использует совмещенный ввод-вывод. С позиции вызывающих функций, класс ведет себя синхронно, если вызывающей функции не надо прервать ввод-вывод. Поток, использующий класс, не может прервать ввод-вывод, так как он блокирует вызов Write(). Все же другой поток может прервать ввод-вывод при необходимости, по причине чего синхронный класс использует совмещенный ввод-вывод.

CSyncMailslotReader

Этот класс реализует читателя почтового ящика и выглядит так:

//  Класс читателя почтового ящика. Используется для чтения из почтового ящика. 
class CSyncMailslotReader : public CMailslot
{
public: CSyncMailslotReader(); virtual ~CSyncMailslotReader();
    virtual bool    Connect(LPCTSTR szSlotname,
                            LPCTSTR szServer = _T("."));
    BYTE            *Read(DWORD& dwBufferLength);
    DWORD            GetMessageCount(
                         LPDWORD pdwNextMessageLength = (DWORD *)  NULL);
};

Как и с классом CSyncMailslotWriter, этот класс предоставляет собственное переопределение метода Connect(), выглядящее так.

//  Создает именованный почтовый ящик. Это должно делаться на локальном компьютере,
//  поэтому параметр имя сервера не используется.
bool CSyncMailslotReader::Connect(LPCTSTR szSlotname, LPCTSTR /*szServer*/)
{
    assert(szSlotname);
   
    if (IsOpen())
    {
        TCHAR szTempSlotname[_MAX_PATH];

        //  Если получаем здесь, это значит, что дескриптор почтового ящика может быть        // действительным, поэтому проверим, что переменная m_pszSlotname не является пустым
        //  указателем. Если она является им, то получено несоответствие,
        //  которого не должно быть.
        assert(m_pszSlotname);
        _sntprintf(szTempSlotname,
                   _MAX_PATH, _T("\\\\.\\mailslot\\%s"), szSlotname);
       
        if (_tcsicmp(m_pszSlotname, szTempSlotname) == 0)
            return true;
        else
            Disconnect();
        }

    //  Удаляем любое ранее созданное имя ящика
    delete m_pszSlotname;
    m_pszSlotname = new TCHAR[_MAX_PATH];
    assert(m_pszSlotname);

    //  Создаем новое имя почтового ящика
    _sntprintf(m_pszSlotname, _MAX_PATH, _T(\\\\.\\mailslot\\%s),
               szSlotname);
    m_pszSlotname[_MAX_PATH - sizeof(TCHAR)] = TCHAR(0);
   
    if ((m_hMailSlot = CreateMailslot(m_pszSlotname, 0,
                                      MAILSLOT_WAIT_FOREVER, NULL))
                    != INVALID_HANDLE_VALUE)
    {
        //  Прикрепляем дескриптор почтового ящика к объекту совмещенного
        //  ввода-вывода.
        m_overlapped.Attach(m_hMailSlot);
        return true;
    }

    return false;
}

Поскольку читатель почтового ящика контролирует время жизни почтового ящика, не надо иметь два метода Connect(). Метод отключается от любого прежнего почтового ящика, контролируемого этим экземпляром класса, и создает новый почтовый ящик. Параметр szServer не используется, потому что, как сказано выше, почтовые ящики должны создаваться на локальном компьютере. После создания почтового ящика его дескриптор прикрепляется к объекту COverlappedIO в этом экземпляре класса.

В начале метода есть проверка корректности, предотвращающая повторный вызов Connect() с тем же самым именем почтового ящика. Без проверки корректности метод бы двигался вперед и закрыл бы дескриптор существующего почтового ящика и заново создал бы его. Это работает, кроме случаев, когда писатели, хранящие открытые дескрипторы почтового ящика, сталкиваются с ошибкой записи при следующей попытке записи. Метод CSyncMailslotWriter::Write() может справиться с этим, но зачем тратить циклы процессора, когда проверка относительно простая?

Читать сообщение из почтового ящика труднее, чем записывать. Причина в том, что есть два разных способа чтения. Можно ждать в цикле, опрашивая класс с помощью GetMessageCount(), или можно фактически войти в метод Read() и ждать сообщение. Использование GetMessageCount() позволяет заранее определить, сколько данных будет прочитано (и сколько памяти выделить), но минус в том, что тратится много циклов процессора на опрос почтового ящика для следующего сообщения. Зато вызов Read() напрямую значит, что неизвестно, сколько данных будет считано, поэтому приходится принимать произвольные решения насчет того, сколько данных вы готовы обработать за один вызов Read(). Мы выбираем второй способ и произвольный предел 65536 байт данных (это число является рекомендованным Microsoft максимальным размером сообщения почтового ящика). Метод Read() выглядит так.

// Читаем сообщение из почтового ящика и возвращаем его в буфер, выделенный
// в куче. Вызывающая функция должна удалить буфер, как только закончит с ним работу.
BYTE *CSyncMailslotReader::Read(DWORD& dwBufferLength)
{
    // Надо выделить большой буфер для входящих сообщений, так как
    // неизвестно, сколько данных поступит
    BYTE  *pbData = (BYTE *) NULL,
          *pbTemp = (BYTE *) NULL;

    dwBufferLength = 0;

    if (IsOpen())
    {
        pbData = new BYTE[65536];
        assert(pbData);

        //  Считываем данные
        if (m_overlapped.Read(pbData, 65536 - sizeof(TCHAR),
                              &dwBufferLength, m_hStopEvent)
            && dwBufferLength)
        {
            //  Если сообщение считано, пора скопировать данные в
            //  буфер нужной длины для хранения сообщения. 
            //  В буфер добавляется один символ, чтобы, если
            //  сообщение действительно является строкой, оно правильно
            //  закончилось и сохранило семантику строки.
            pbTemp = new BYTE[dwBufferLength + sizeof(TCHAR)];
            assert(pbTemp);
            memcpy(pbTemp, pbData, dwBufferLength);
            pbTemp[dwBufferLength] = TCHAR(0);
        }
    }

    delete [] pbData;
    return pbTemp;
}

Здесь есть хитрость. Почтовые ящики основаны не на строках, а на байтах, и им можно отправлять любые данные. Но в основном почтовые ящики применяются для отправки текстовых строк от одного процесса другому, и семантика строк должна сохраняться. Резервирование места для признака конца NULL в конце буфера обеспечивает это. Реальный предельный размер сообщения в этой реализации составляет 65536 байт минус sizeof(размер) символа кодировки.