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

ОГЛАВЛЕНИЕ

Данная статья повествует о том, как использовать почтовые ящики для связи между процессами.

•    Скачать проект перечислителя почтовых ящиков - 24 Кб
•    Скачать проект слушателя почтового ящика - 22 Кб
•    Скачать проект писателя почтового ящика - 22 Кб
•    Скачать исходники библиотеки почтового ящика - 20 Кб

Введение

Иногда надо, чтобы одна программа взаимодействовала с другой. У вас может быть много серверов, работающих на множестве машин, и вам надо дистанционно управлять одним или более серверами из центрального места. Windows предоставляет много методов связи –  от сокетов посредством именованных каналов до DDE(динамический обмен данными), DCOM(распределённая компонентная модель объектов) и почтовых ящиков. В этот раз рассматриваются почтовые ящики. Предполагается небольшое знание API (интерфейсов программирования приложений) CreateFile(), ReadFile() и WriteFile() и основ совмещенного ввода-вывода.

Почтовые ящики

Реализует протокол 'много писателей/один читатель'. Процесс создает почтовый ящик по имени и затем ждет, когда в него запишутся сообщения. Другие процессы могут открыть почтовый ящик, если они знают его имя, и писать сообщения в него. Можно иметь лишь одного читателя почтового ящика, но можно иметь много писателей почтового ящика. Microsoft использует терминологию сервер/клиент для описания этого. По терминологии Microsoft, сервер создает и читает из почтового ящика; клиент подключается к имеющемуся почтовому ящику и пишет в него. Это слегка сбивает с толку – удобней мыслить в терминах читателей почтового ящика и писателей почтового ящика.

Почтовые ящики проявляют интересное и очень полезное свойство. Кто-то записывает сообщение в почтовый ящик, и читатель получает сообщение. Сообщение является целым блоком данных произвольной длины. Если писатель записывает 60 байт, читатель читает 60 байт - ни больше, ни меньше. Если писатель записывает 327 байт, читатель читает 327 байт – вы понимаете идею. Этот протокол основан на сообщениях, а не на байтах. Он сродни режиму передачи сообщений в именованном канале. Это не значит, что нельзя прочитать лишь часть сообщения; это значит, что естественный способ использования почтового ящика основан на сообщениях, что отражено в API(интерфейс программирования приложений), доступном читателю.

Почтовые ящики работают через сеть. Вы задаете имя почтового ящика в формате UNC(общее соглашение об именовании), так же, как вы бы задали имя файла на сервере где-то в вашей сети. Если бы вы имели два процесса, работающие на одном и том же компьютере, и использовали бы почтовые ящики для связи между ними, вы бы создали почтовый ящик путем задания имени \\.\mailslot\slotname, и процесс, подключаясь к этому почтовому ящику, использовал бы это же имя. Видите точку в имени? Это псевдоним для компьютера, на котором работает процесс, в данном случае, означает «искать почтовый ящик на локальном компьютере». Чтобы поставить сеть приложения в известность, вы бы заменили точку на имя компьютера, на котором работает процесс, создавший почтовый ящик. Итак, если бы у нас было два компьютера, названные, соответственно, Rob и Chris, и процесс, работающий на компьютере под именем Chris, создал бы почтовый ящик под именем cp, процесс, работающий на компьютере под именем Rob, смог бы подключиться к этому почтовому ящику, используя имя \\chris\mailslot\cp.


Создание почтового ящика

Выполняется путем вызова API CreateMailslot(), передачи ему имени почтового ящика в формате UNC и ряда других параметров. Вы можете создать почтовый ящик только на локальном компьютере, поэтому серверная часть пути UNC должна разрешаться в локальный компьютер; то есть, это должно быть имя локального компьютера или точка '.' - использовать '.' проще всего. Другие параметры, по порядку: максимальный размер сообщения, которое может быть отправлено почтовому ящику, a также время ожидания, определяющее, как долго читатель почтового ящика будет ждать сообщение, и дескриптор защиты, определяющий, может ли дескриптор, возвращенный API CreateMailslot(), быть унаследован дочерним процессом.

Подключение к почтовому ящику

Выполняется просто. Вы используете API CreateFile(), указывая имя почтового ящика в формате UNC. Будьте осторожны с режимами совместного доступа при открытии почтового ящика, если вы реализуете модель много писателей/один читатель. Если писатель почтового ящика открывает почтовый ящик без указания FILE_SHARE_WRITE в качестве режима совместного доступа, он не позволит ни одному следующему писателю писать в почтовый ящик. Неприятный момент в реализации API заключается в том, что последующие вызовы CreateFile() удаются при условии, что они получают обратно действительный дескриптор почтового ящика, но все записанное с помощью этого дескриптора теряется.

Что делать с дескриптором почтового ящика после его получения?

Если вы создали почтовый ящик с помощью API CreateMailslot(), вы читаете из него с помощью API ReadFile(). Дескриптор почтового ящика создается в режиме совмещенного ввода-вывода, поэтому на нем можно использовать совмещенный ввод-вывод, хотя можно использовать не совмещенный ввод-вывод, если он лучше подходит для вашей модели. Вы также можете вызвать API GetMailslotInfo(), чтобы запросить, сколько сообщений ждут, какова длина следующего сообщения, и каково время ожидания. Вы можете вызвать API SetMailslotInfo(), чтобы изменить время ожидания. Знайте, что дескриптор, передаваемый этим двум API, должен быть создан API CreateMailslot().

Если вы не создали почтовый ящик, то подключайтесь к почтовому ящику с помощью API CreateFile(). В этом случае вы можете писать в него с помощью API WriteFile(). Можете ли вы использовать совмещенный ввод-вывод, зависит от способа вызова API CreateFile(); он бывает синхронным или асинхронным, в зависимости от ваших нужд. Нельзя подключиться к почтовому ящику с помощью CreateFile() и иметь возможность читать из него.

Глюк почтовых ящиков

Документация MSDN по почтовым ящикам говорит, что почтовый ящик существует, пока существует любой открытый дескриптор почтового ящика. Это неверно (в Windows XP Professional SP2). Может иметься любое количество открытых дескрипторов писателя для почтового ящика, но почтовый ящик исчезает, как только закрывается дескриптор читателя (и записи в почтовый ящик скрываются, как только дескриптор читателя закрывается). Это логично. Если можно иметь лишь одного читателя, нет смысла в существовании почтового ящика после того, как читатель исчез, так как любые сообщения, записанные в почтовый ящик, будут напрасно помещаться в буфер системой; если нет читателя, помещенные в буфер сообщения будут висеть там вечно (помните, что нельзя использовать CreateFile() для открытия дескриптора чтения для почтового ящика).

Другие рекомендации по почтовым ящикам

Если вы внимательно прочитаете документацию MSDN, то поймете, что мы несколько упростили правила при подключении писателя к существующему почтовому ящику. Наряду с указанием имени сервера, подключающегося к почтовому ящику на конкретном сервере, можно задать подключение ко всем почтовым ящикам с конкретным именем в домене. Это делается путем указания имени домена, а не имени сервера, следующим образом: \\domainname\mailslot\name. Также можно использовать звездочку * как условное обозначение для главного домена. На первый взгляд, это кажется прекрасным – можно создать любое количество читателей, работающих на разных компьютерах внутри домена, и писать во все из них одновременно путем задания имени домена. Но есть минус. Если вы подключаетесь к почтовому ящику в качестве писателя с помощью описателя домена, вы не можете записать в сообщении больше 424 байт. Зато, если ваше приложение может выдержать такое ограничение, это самый легкий способ контролировать один процесс из множества мест в домене.

То, что вы прочитали до сих пор, могло вызвать у вас интерес к использованию почтовых ящиков; остальная часть статьи описывает набор классов, написанных для облегчения использования почтовых ящиков. Всего есть пять классов.

Первый класс, CMailslot, является абстрактным базовым классом, инкапсулирующим основные составляющие почтовых ящиков.

Еще есть два производных класса: CSyncMailslotReader –  реализующий сервер (читатель) почтового ящика, и CSyncMailslotWriter –  реализующий клиент (писатель) почтового ящика. С помощью этих двух классов поток, вызывающий соответствующую функцию-член (читать или писать), блокируется до тех пор, пока операция не завершится.

Затем есть CQueuedMailslotWriter, формирующий очередь сообщений и использующий экземпляр CSyncMailslotWriter в отдельном потоке, чтобы фактически записывать сообщения. Этот класс асинхронный, отделяющий писателя от задержек в сети. Процесс писателя может поместить в очередь много тысяч сообщений при желании, пока основной поток продолжает генерировать сообщения; класс CQueuedMailslotWriter обрабатывает детали их отправки в почтовый ящик, когда почтовый ящик в состоянии принять их.

Наконец, есть один высокоуровневый класс, CAsyncMailslotReader, реализующий простой протокол, позволяющий читателю рассматривать входящие сообщения как события.
Нет класса читателя почтового ящика с поддержкой очередей. Было бы просто написать его, но нет смысла писать оба класса. Либо оба конца способны угнаться за потоком сообщений, либо один конец строит в очереди. Принуждение обоих концов помещать сообщения в очередь перекладывает ответственность с писателя на читателя.


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(размер) символа кодировки.


CQueuedMailslotWriter

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

class CQueuedMailslotWriter : public CSyncMailslotWriter
{
    class CQueuedData
    {
    public:
        CQueuedData(BYTE *pbData, DWORD dwDataLength);
        ~CQueuedData();

        DWORD       Length() const      { return m_dwDataLength; }
        BYTE        *Data() const       { return m_pbData; }

    private:
        BYTE        *m_pbData;
        DWORD       m_dwDataLength;
    };

    typedef deque<CQUEUEDDATA *> DATAQUEUE;
    typedef DATAQUEUE::const_iterator DQITER;

public:
                    CQueuedMailslotWriter(void);
    virtual         ~CQueuedMailslotWriter(void);

    virtual bool    Write(BYTE *pbData, DWORD dwDataLength,
                          BOOL bImportant);
    virtual bool    Connect(LPCTSTR szSlotname,
                            LPCTSTR szServername = _T("."));

private:
    static unsigned __stdcall ThreadStub(LPVOID data);
    virtual void    ThreadProc(CBaseThread *pThread);
    void            StopThread();

    HANDLE          m_hStopEvent,
                    m_hSignalEvent,
                    m_haSignal[2];
    CInterruptibleMutex m_imMutex;
    CBaseThread     *m_pThread;
    volatile bool   m_bStop;
    DATAQUEUE       m_highPriorityDataQueue,
                    m_normalPriorityDataQueue;
};

Основные дополнения – закрытый класс CQueuedData и несколько связанных с потоками переменных. Класс CQueuedData – просто удобный способ сохранения данных, передаваемых в каждом вызове метода Write(). Метод Write() упаковывает переданные данные и добавляет их в очередь. Позже метод ThreadProc() выведет данные из очереди и передаст их методу Write() базового класса. CQueuedMailslotWriter::Write() выглядит так:

//  Записывает сообщение в почтовый ящик. Он помещает сообщение в очередь
//  для почтового ящика, а запись фактически производит фоновый поток
bool CQueuedMailslotWriter::Write(BYTE *pbData, DWORD dwDataLength,
                                  BOOL bImportant)
{
    assert(pbData);
    assert(dwDataLength);

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

    if (IsOpen())
    {
        //  Сперва захватываем мьютекс. Вы должны иметь мьютекс перед
        //  попыткой создания объекта QueuedData, иначе вы
        //  повредите кучу программы или подвесите блокировку кучи.
        if (m_imMutex.AquireMutex(m_hStopEvent) ==
                                  CInterruptibleMutex::eMutexAquired)
        {
            CQueuedData *pqData = new CQueuedData(pbData, dwDataLength);

            assert(pqData);

            if (bImportant)
                //  Сообщение с высоким приоритетом, помещаем его в высокоприоритетную
                //  очередь
                m_highPriorityDataQueue.push_back(pqData);
            else
                //  Сообщение с нормальным приоритетом, помещаем его в очередь
                //  с нормальным приоритетом
                m_normalPriorityDataQueue.push_back(pqData);

            m_imMutex.ReleaseMutex();

            //  Оповещаем поток обработчика очереди...
            SetEvent(m_hSignalEvent);
            return true;
        }
    }

    return false;
}

Как и с методом CSyncMailslotWriter::Write(), этот класс сначала проверяет, что подключение к почтовому ящику открыто. Если нет, то он пытается вновь подключиться к почтовому ящику. Если он имеет то, что, по его мнению, является действительным подключением к почтовому ящику, он двигается вперед и добавляет сообщение в очередь. Если нет –  он отбрасывает сообщение. Если класс добавил сообщение в очередь – он устанавливает событие, сообщающее фоновому потоку, что пришло новое сообщение и нуждается в отправке. Метод Write() и процедура потока совместно используют одни и те же очереди данных, поэтому они должны реализовывать безопасный метод добавления и удаления записей из очереди. Они делают это путем использования общего мьютекса (взаимная блокировка) (фактически, экземпляр класса CInterruptibleMutex)

Поток выглядит так:

void CQueuedMailslotWriter::ThreadProc(CBaseThread *pThread)
{
    CQueuedData *pqData;
    DQITER      pdqIterator;
    bool        bQueuePriority;

    while (!pThread->Stop())
    {
        switch (WaitForMultipleObjects(2, m_haSignal, FALSE, INFINITE))
        {
        case WAIT_OBJECT_0:
            //  Приказано остановиться, поэтому останавливаемся
            break;

        case WAIT_OBJECT_0 + 1:
            //  Захватываем мьютекс, прежде чем войти в цикл
            if (m_imMutex.AquireMutex(m_hStopEvent) !=
                                      CInterruptibleMutex::eMutexAquired)
                //  Приказано остановиться, поэтому останавливаемся...
                break;

            //  Новое сообщение добавлено в очередь, отправляем его
            while ((m_highPriorityDataQueue.size() ||
                    m_normalPriorityDataQueue.size())
                   && !pThread->Stop())
            {
                //  Продолжаем выполнять цикл, пока очередь не опустеет, или
                //  не прикажут остановиться.
                if (m_highPriorityDataQueue.size())
                {
                    pdqIterator = m_highPriorityDataQueue.begin();
                    bQueuePriority = false;
                }
                else
                {
                    pdqIterator = m_normalPriorityDataQueue.begin();
                    bQueuePriority = true;
                }

                pqData = *pdqIterator;

                //  Пока закончили, снимаем мьютекс, чтобы дать другим потокам
                //  возможность поместить данные в очередь.
                m_imMutex.ReleaseMutex();
               
                if (CSyncMailslotWriter::Write(pqData->Data(),
                        pqData->Length()) == pqData->Length())
                {
                    //  Снова захватываем мьютекс, чтобы удалить сообщение
                    //  из очереди
                    if (m_imMutex.AquireMutex(m_hStopEvent) ==
                                  CInterruptibleMutex::eMutexAquired)
                    {
                        //  Флаг bQueuePriority сообщает, из какой очереди
                        //  извлечено сообщение. Здесь нельзя использовать
                        //  размер очереди, так как очереди могли измениться
                        //  за время между текущим моментом и моментом
                        //  извлечения сообщения для отправки.
                        if (bQueuePriority == false)
                            m_highPriorityDataQueue.pop_front();
                        else
                            m_normalPriorityDataQueue.pop_front();

                        delete pqData;
                        continue;
                    }
                }
                else
                    //  Не удалось записать сообщение, оставляем его в очереди
                    //  и прерываем цикл писателя
                    break;
            }

            //  Цикл завершен, поэтому снимаем мьютекс.
            m_imMutex.ReleaseMutex();
            break;
        }
    }

    // Убеждаемся, что любой ожидающий ввод-вывод отменен, перед выходом из потока.
    if (IsOpen())
        CancelIo(m_hMailSlot);
}

Он ждет, пока ему не сообщат о приходе нового сообщения. Когда сообщение приходит, он сначала проверяет высокоприоритетную очередь, а затем очередь с нормальным приоритетом. В любом случае, он получает сообщение для отправки путем вызова CSyncMailslotWriter::Write(). Главное в этой процедуре - блокировка. Когда ему сообщают, что сообщение ждет отправки, он блокирует очередь сообщений, захватывая мьютекс. Он извлекает сообщение из одной или другой очереди и снимает мьютекст перед попыткой отправкой сообщения. После отправки сообщения он снова захватывает мьютекс, чтобы безопасно удалить сообщение из очереди. Цикл усложняется необходимостью следить, чтобы мьютекс был захвачен перед проверкой размеров и очередей, и поскольку размеры очередей являются управляющими переменными для цикла while, это означает больше вызовов для захвата мьютекса, чем ожидалось. Так как вызов CSyncMailslotWriter::Write() может блокироваться, мы снимаем мьютекс перед осуществлением этого вызова. После того как вызов вернул управление, надо снова захватить мьютекс перед переходом к началу цикла while.

CAsyncMailslotReader

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

Класс выглядит так:

class CAsyncMailslotReader : public CSyncMailslotReader
{
public:
                    CAsyncMailslotReader();
    virtual         ~CAsyncMailslotReader();

    virtual bool    OnMessage(BYTE *pbMessage, DWORD dwMessageLength) = 0;
    virtual bool    Connect(LPCTSTR szSlotName);

protected:
    static unsigned int __stdcall ThreadStub(LPVOID data);
    unsigned int    ThreadProc(LPVOID data);

    CBaseThread     *m_pThread;
};

Класс переопределяет виртуальный метод Connect(), чтобы создать контролирующий поток в момент, когда объект создает почтовый ящик. Connect() выглядит так:

bool CAsyncMailslotReader::Connect(LPCTSTR szSlotName)
{
    assert(szSlotName);
    bool bStatus = CSyncMailslotReader::Connect(szSlotName);

    if (bStatus)
    {
        //  Если почтовый ящик был успешно создан, запускается поток для
        //  контроля за ним.
        m_pThread = new CBaseThread(m_hStopEvent, &m_bStop, ThreadStub,
                                    false, this);
        assert(m_pThread);
    }

    return bStatus;
}

Что весьма просто. Процедура потока выглядит так:

unsigned int CAsyncMailslotReader::ThreadProc(LPVOID data)
{
    CBaseThread *pThread = (CBaseThread *) data;
    BYTE        *pbMessage;
    DWORD       dwMessageLength = 0;

    assert(pThread);

    while (!pThread->Stop())
    {
        //  Получаем и отправляем сообщения
        pbMessage = Read(dwMessageLength);

        if (dwMessageLength)
            OnMessage(pbMessage, dwMessageLength);
    }

    return 0;
}

Процесс просто сидит внутри вызова CSyncMailslotReader::Read(), ожидая следующего сообщения. Когда это сообщение приходит, поток вызывает виртуальный метод OnMessage(), который вы переопределяете, чтобы выполнять любую нужную для вашего приложения обработку. Заметьте, что метод CAsyncMailslotReader::OnMessage() является чисто виртуальным; вы не можете напрямую создать экземпляр CAsyncMailslotReader, но должны унаследовать свой собственный класс от него.

Знайте, что вызов вашего метода OnMessage() происходит в потоке, отличном от главного потока вашего приложения; это особенно важно помнить, если вы используете класс в приложении MFC(библиотека базовых классов Microsoft) и манипулируете CWnd из метода OnMessage().


Использование кода

Вы должны собрать библиотеку. Это статическая библиотека, а не динамическая. Загружаемые исходники содержат все файлы, необходимые для сборки библиотеки. Включите соответствующие заголовки в ваш проект, в зависимости от того, какие неабстрактные классы ваш проект использует; они будут включать в себя mailslots.h, а этот файл, в свою очередь, вставляет необходимые операторы для добавления библиотеки. Вам остается лишь убедиться, что библиотека находится в путях библиотек.

Демонстрационные проекты

Есть три демонстрационных проекта. Первый на самом деле не использует библиотеку почтового ящика; он лишь перечисляет и отображает почтовые ящики, имеющиеся в вашей системе. Он был написан во время начального изучения почтовых ящиков и служил для определения доступных почтовых ящиков. Принесет ли он вам пользу?

Второй демонстрационный проект – слушатель почтового ящика. Вы указываете имя почтового ящика и нажимаете кнопку «Создать». Как только вы сделаете это, почтовый ящик с этим именем будет существовать в вашей системе, пока программа продолжает работать. Любые сообщения, отправленные этому почтовому ящику, будут отображены программой. Так как это слушатель почтового ящика, как только вы закрываете программу, почтовый ящик исчезает.

Третий демонстрационный проект – писатель почтового ящика. Как и для демонстрационного проекта слушателя почтового ящика, вы указываете имя почтового ящика и нажимаете кнопку «Создать». Если почтовый ящик с таким именем существует, писатель соединится с ним. Затем вы сможете набрать сообщение в поле ввода сообщения, нажать кнопку «Отправить» и увидеть, как слушатель почтового ящика отобразит сообщение.

Известные ошибки

Известна одна ошибка. Она возникает в сценарии, столь маловероятном в реальных приложениях, что не стоит о ней беспокоиться. Если вы запустите проект слушателя почтового ящика и проект писателя почтового ящика, создадите почтовый ящик в слушателе, подключитесь к слушателю, отправите одно или несколько сообщений и затем измените имя в слушателе, согласуете изменение с писателем и отправите повторные сообщения, то увидите, что каждое второе сообщение - чушь. Эту ошибку легко воспроизвести. Но ее появление маловероятно при реальном использовании класса, так как вряд ли слушатели почтового ящика будут менять имя слушаемого ими почтового ящика, не говоря об оповещении писателей, что они собираются изменить имя почтового ящика.

Почтовые ящики в сравнении с именованными каналами

По нашему опыту, почтовые ящики немного легче использовать для связи между процессами, чем именованные каналы. Все же это не простое проектное решение. Если бы это было так, не существовало бы и то, и другое. Поэтому здесь есть плюсы и минусы.

Как сказано в начале статьи, почтовые ящики реализуют модель много писателей/один читатель. Один дескриптор читателя почтового ящика может контролировать много писателей. Именованные каналы требуют один канал на каждое соединение. Отсюда следует, что реализация модели много писателей/один читатель требует один именованный канал на каждого писателя и один дескриптор чтения на каждого писателя. Это устанавливает предел того, сколько писателей может контролировать реализация именованного канала - 64 писателя на каждый контролирующий поток.

К тому же почтовые ящики можно создавать в любой 32-битной реализации Windows, включая Windows 95. Это позволяет в любой момент включить семейство операционных систем Win9X в целевую аудиторию. Система Win9X может подключиться к именованному каналу, но не может создать его – для создания именованного канала нужны Windows NT и потомки.

Зато почтовые ящики используют датаграммы UDP(протокол датаграмм пользователя дейтаграмма) для передачи. Это значит, что доставка сообщения не гарантирована. Это значит, что не гарантировано получение сообщений в порядке их отправки. Если для вашего приложения важно, чтобы сообщения были получены в порядке отправки, почтовые ящики не обязательно подойдут вам. Вы можете использовать почтовые ящики в качестве механизма связи между процессами на одной и той же машине - шансы потерять сообщение или отправить не по порядку крайне малы на локальном компьютере (но будьте осторожны, если ваш локальный компьютер многопроцессорный). Если вы отправляете сообщение через сеть, и каждое сообщение обязательно должно прийти в верной последовательности, ищите другой механизм связи между процессами.

Наконец, именованные каналы являются двусторонним каналом связи. Каждый конец канала может читать и писать из одной и той же трубы. Почтовый ящик не позволяет такого.