PIPE. Сетевое взаимодействие

PIPE. Сетевое взаимодействие

Привет всем, оторвавшимся от монитора хотя бы на время для того, чтобы почитать газету, что не может не радовать (это значит, что вы еще не совсем потеряны для общества). Но спешу вас огорчить, поговорить сегодня мне хочется не о самой простой вещи. А расскажу я вам о еще одной технологии межпрограммного взаимодействия, придуманного для облегчения(?) нелегкого труда программистов.

Некоторое время назад я показал пример программ (клиента и сервера), которые могли обмениваться данными при помощи технологии MailSlot (см КГ № 7'2004). Но была изобретена еще одна технология межпрограммного взаимодействия. Получила она название именованных каналов. Если по-простому, то два компьютера образуют "трубу" для обмена данными между собой. Активно данная технология начала использоваться в WindowsNT. Типичный пример — Microsoft SQL Server довольно активно ее эксплуатирует. Тем не менее, информации по этому вопросу не так уж и много. А зря:-). Ведь в отличие от технологии MailSlot, посылать сообщение по этой "трубе" могут как клиент, так и сервер. (Там, если вы помните, сервер создавал мэилслот, откуда мог считывать информацию, записанную туда клиентом, но клиент не мог читать сообщения). Здесь ситуация получше. Сервер создает канал, после чего подключившийся клиент может как отсылать серверу информацию, так и считывать оную, отосланную ему сервером. Подробнее весь этот процесс мы рассмотрим ниже. Итак, вы, наверное, уже поняли, что для клиента PIPE-канал является таким же "устройством", как и обычные файлы, с которых можно читать и в которые можно писать. Обозначим нашу задачу. Нам нужно две программы (клиент и сервер), которые смогли бы обмениваться данными. Приведу основные функции, необходимые для работы с каналами. И первая из них — создание канала:

"HANDLE CreateNamedPipe(
LPCTSTR lpName, // Имя PIPE канала
DWORD dwOpenMode, // Параметры открытия pipe
DWORD dwPipeMode, // Спецификация pipe канала
DWORD nMaxInstances, // "Нормировка" клиентов
DWORD nOutBufferSize, //Размер выходного буфера
DWORD nInBufferSize, //Размер входного буфера
DWORD nDefaultTimeOut, //Таймаут в миллисекундах
LPSECURITY_ATTRIBUTES lpSecurity Attributes // Атрибуты безопасности (обычно //NULL, других значений я как-то не встречал)
);"

Функция возвращает HANDLE на созданный канал. lpName задается в виде \\.\pipe\ pipename. Далее второй параметр (dwOpenMode) задает способ доступа к каналу и может принимать следующие значения:
PIPE_ACCESS_DUPLEX — и клиент и сервер имеют доступ к записи и чтению информации из канала (эквивалентно GENERIC_READ | GENERIC_WRITE).
PIPE_ACCESS_INBOUND — только клиент имеет доступ к каналу. Это значение эквивалентно GENERIC_READ доступ для сервера и клиент должен иметь GENERIC_WRITE права.
PIPE_ACCESS_OUTBOUND — все с точностью до наоборот. Соответственно GENERIC_WRITE для сервера и GENERIC_READ для клиента.
dwPipeMode может принимать следующие значения:

Параметры записи и чтения:
PIPE_TYPE_BYTE — данные записываются как поток байтов (разбор потока возложен на приложение). Не может использоваться совместно с PIPE_READMODE_MESSAGE
PIPE_TYPE_MESSAGE — данные представляют собой поток сообщений. Этот способ может использоваться или с PIPE_READMODE_MESSAGE или с PIPE_READMODE_BYTE.

Параметры чтения:
PIPE_READMODE_BYTE — чтение из канала происходит в бинарном режиме, т.е. так же, как при бинарном режиме открытия файлов. Может использоваться с параметрами записи и чтения PIPE_TYPE_MESSAGE или PIPE_TYPE_BYTE. Подходит для передачи, например, видеопотока по сети.
PIPE_READMODE_MESSA-GE — чтение сообщений происходит в "текстовом" режиме. Подходит в том случае, если необходимо передать текстовое сообщение ("God is God!" например).

Параметры "ожидания" канала:
PIPE_WAIT — позволяет блокировать канал, если клиент не подключен, в канал производится запись и т.д.
PIPE_NOWAIT — используется при асинхронном режиме передачи данных.
Значение nMaxInstances — это по сути дела максимальное количество подключений. Может принимать значение PIPE_ UNLIMITED_INSTANCES, т.е. лимит ограничивается только ресурсами машины.

ConnectNamedPipe() и Dis-connectNamedPipe() используются для подключения (и, соответственно, отключения) к каналу, если известен HANDLE на него, возвращаемый Create NamedPipe(). Информацию о канале можно получить при помощи GetNamedPipeHandle State() и GetNamedPipeInfo(). HANDLE на канал должен быть возвращен CreateNamedPipe(). Информация может быть получена о размере буфера, клиенте и др. (Подробнее смотри в MSDN:-)). А эта функция позволяет получить данные из канала, причем последние из него не будут удалены (иногда полезно, поэтому приведу ее описание несколько подробнее):

BOOL PeekNamedPipe(
HANDLE hNamedPipe, // handle для pipe
LPVOID lpBuffer, // Буфер для данных
DWORD nBufferSize, // Размер буфера для данных
LPDWORD lpBytesRead, // Количество читаемых байтов
LPDWORD lpTotalBytesAvail, // Количество доступных для чтения байт
LPDWORD lpBytesLeftThisMessage // Количество непрочитанных данных
);

hNamedPipe — HANDLE на канал. Может быть возвращен как CreateNamedPipe, так и CreateFile.
SetNamedPipeHandleState() позволяет изменить параметры уже созданного канала.
CallNamedPipe() позволяет подключиться к каналу, передать данные и закрыть HANDLE.

Клиент может подключаться к PIPE при помощи функции CreateFile(), довольно подробное описание которой я давал в статье о мэилслотах, а посему приводить здесь я его не буду. Единственное, что первый параметр (т.е. имя) канала должно задаваться в виде "\\servername\ pipe\pipename". Чтение производится, опять же, как и в случае с MailSlot, ReadFile(), которой необходимо передать HANDLE, возвращаемый CreateFile().
А теперь перейдем к практической части нашего повествования. А именно — написанию сервера и клиента, работающих через эти самые пресловутые PIPE — каналы. Начнем с сервера. Использовать будем Visual C++ (и соответственно MFC). Программа будет называться PIP_serv. Создаем проект с таким названием, основанный на диалоге, и проектируем интерфейс примерно таким образом:



Привожу таблицу, по которой следует задать параметры элементов и функции — обработчики. Да, "большой EDIT" следует сделать многострочным с автоскроллингом. (Если кто не знает — по щелчку правой кнопки мыши по элементу — заходим в Properties, а затем на вкладку Styles. Там это все и можно сделать, установив в соответствующих местах галочки: Multiline,Vertical scroll, Auto Vscroll, Auto HScroll). Кнопочку EXIT переделаем из кнопки OK. Собственно сама таблица:



Дополнительно нам понадобятся следующие переменные, принадлежащие классу CPIP_ servDlg:
BOOL OK — флаг успешного создания канала и подключения клиента.
HANDLE piphannal — будет использоваться для хранения HANDLE на канал. Еще создадим две дополнительные функции: "BOOL CPIP_servDlg:: con_asq()" и "void CPIP_serv Dlg::connect(CString name)".
Общая схема работы сервера выглядит примерно следующим образом: при нажатии на кнопку Read вызывается ее обработчик void CPIP_servDlg:: OnRead(), который в свою очередь при не установленном флаге OK вызывает функцию создания канала CPIP_servDlg:: Onconnect(), где выполняется CPIP_servDlg::connect(name) (собственно создание канала) и CPIP_servDlg::con_asq() (ожидание подключения клиента и обработка возможных ошибок). После успешного подключения клиента производится считывание данных (при помощи ReadFile()) и теперь можно по желанию пользователя отправлять информацию клиенту (введя ее в форму Send_this и нажав кнопку Send). Отсылка производится функцией void CPIP_servDlg::OnsendBT() — обработчиком нажатия этой кнопки. Ниже приведем собственно исходный код изменяемых нами функций. И первая — обработчик кнопки Read "void CPIP_servDlg::OnRead()":

"void CPIP_servDlg::OnRead()
{
char *temp;
char szBuf[512];
DWORD cbRead;
//Если канал не был создан ранее, он создается и начинается ожидание клиента
if(OK!=TRUE)
CPIP_servDlg::Onconnect();
// Считываем данные из канала Pipe
if(ReadFile(piphannal, szBuf, 512, &cbRead, NULL))
{
//Распечатываем данные пользователю
m_mess+=(CString)&szBuf[0];
m_mess += "\r\n";
UpdateData(FALSE);
}
else
{
//Информируем о возможных ошибках
m_mess+="ReadFile: Error";
itoa(GetLastError(), temp, 10 );
m_mess+=(CString)&temp[0];
UpdateData(FALSE);
return;
}"
Я думаю, особых пояснений не требуется, кроме уже имеющихся комментариев.
Исходник CPIP_servDlg::On connect():

"void CPIP_servDlg::Onconnect()
{
//Задаем имя канала
LPSTR name = "\\\\.\\pipe\\$Pipe_1$";
//Создаем канал
CPIP_servDlg::connect(name);
//Подключаем клиента
CPIP_servDlg::con_asq();
}".

Обработчик нажатия кнопки Send должен выглядеть следующим образом:

"void CPIP_servDlg::OnsendBT()
{
//Обновляем форму
UpdateData(TRUE);
char *szBuf;
DWORD cbWritten;
//Преобразуем данные
szBuf=m_string.GetBuffer(m_mess.GetLength());
// Посылаем введенную строку и обрабатываем возможную ошибку
if(!WriteFile(piphannal, szBuf, strlen (szBuf) + 1,&cbWritten, NULL))
{MessageBox("Ошибка записи.");
return;
};
//Обнуляем форму
m_string="";
UpdateData(FALSE);
}"

При нажатии на Send, введенная в форму строка будет записана в канал.
Собственно функция создания PIPE — канала выглядит так:

"void CPIP_servDlg::connect(CString name)
{
char *temp;
m_mess+="Сервер PIPE — канала\r\n";
UpdateData(FALSE);
// Создаем канал Pipe, имеющий имя name
piphannal = CreateNamedPipe(
name,
PIPE_ACCESS_DUPLEX, // С возможностью доступа как со стороны сервера, так и //клиента
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
512, 512, 5000, NULL);
//Обрабатываем возможные ошибки, если канал не был создан успешно
if(piphannal == INVALID_HANDLE_VALUE)
{
//Выводим информацию пользователю
m_mess+=" Ошибка функции Cre-ateNamedPipe: Error %ld\n",
itoa(GetLastError(), temp, 10 );
m_mess+=(CString)&temp[0];
UpdateData(FALSE);
return;
}
return;
}"

И нам осталась последняя функция в теле сервера, та, что отвечает за подключение клиента:

"BOOL CPIP_servDlg::con_asq()
{
BOOL fCon;
char *temp;
m_mess+="Ожидаем соединения с клиентом. \r\n";
// Ожидаем соединения со стороны клиента
fCon = ConnectNamedPipe(piphannal, NULL);
// Обрабатываем ошибки, которые могли возникнуть при подключении
if(!fCon)
{
switch(GetLastError())
{
case ERROR_NO_DATA:
m_mess+="Ошибка функции Con-nectNamedPipe: ERROR_NO_DATA";
UpdateData(FALSE);
CloseHandle(piphannal);
return FALSE;
break;
case ERROR_PIPE_CONNECTED:
m_mess+=" Ошибка функции Connect NamedPipe: ERROR_PIPE_CONNECTED";
UpdateData(FALSE);
CloseHandle(piphannal);
return FALSE;
break;
case ERROR_PIPE_LISTENING:
m_mess+=" Ошибка функции Connect NamedPipe: ERROR_PIPE_LISTENING";
UpdateData(FALSE);
CloseHandle(piphannal);
return FALSE;
break;
case ERROR_CALL_NOT_IMPLEMENTED:
m_mess+=" Ошибка функции Connect NamedPipe: ERROR_CALL_NOT_IMPLEMENTED";
UpdateData(FALSE);
CloseHandle(piphannal);
return FALSE;
break;

default:
itoa(GetLastError(), temp, 10 );
m_mess+=" Ошибка функции ConnectNamedPipe: Error r\\n",
m_mess+=(CString)&temp[0];
UpdateData(FALSE);
CloseHandle(piphannal);
return FALSE;
break;

}
UpdateData(FALSE);
return TRUE;
}

m_mess+="\r\nПодключение прошло успешно.\r\n";
UpdateData(FALSE);
//Устанавливаем флаг того, что создание канала и подключение клиента прошло успешно
OK=TRUE;
}"

Ну вот, пожалуй, и все с сервером. Но сервер без клиента работать не сможет. А посему, открываем новый проект при помощи MFC AppWizzard, даем ему название PIP_clint и создаем примерно такой интерфейс. (Я рекомендую начинающим на первых порах давать названия программам такие же, как и в приведенных примерах, тогда будет меньше опечаток и ошибок. А еще, если не хотите мучаться, всегда можно попросить меня выслать исходник. Буду рад помочь)).



Как обычно, кнопочку "EXIT" делаем из кнопки "OK". "Большой" Edit делаем многострочным(иначе строки не будут переносится, все будет в одной) и с "автовертикальным" скроллингом. Остальным элементам даем имена и обработчики согласно таблице.



Дополнительно нам понадобится еще одна функция void CPIP_clintDlg::CreateF(), как вы, наверное, уже поняли, класса CPIP_clintDlg, и переменная flag типа BOLL для указания на то, что соединение с сервером прошло успешно. Вновь созданная функция должна иметь такое вот содержание:

"void CPIP_clintDlg::CreateF()
{
//Если работаем по сети, то имя канала должно иметь вид "\\servername\pipe\pipename"
char *name="\\\\.\\pipe\\$Pipe_1$";
char *temp;
// Создаем канал с именем name
pipehandle = CreateFile(
name, GENERIC_READ | GENERIC_WRITE,
0,NULL, OPEN_EXISTING, 0, NULL);
//Обрабатываем возможные ошибки
if(pipehandle == INVALID_HANDLE_ VALUE)
{
m_mess+="Ошибка функции Cre-ateFile: Error";
itoa(GetLastError(), temp, 10 );
m_mess+=(CString)&temp[0];
UpdateData(FALSE);
return;
}
m_mess+="ON LINE";
//Устанавливаем флаг
flag=TRUE;
return;
}"

Далее приведу тела функций — обработчиков кнопок. И первой будет обработчик кнопки SEND:

"void CPIP_clintDlg::OnSendBt()
{
// Количество байт, переданных через канал
DWORD cbWritten;
// Буфер для передачи данных
char *szBuf;
UpdateData(TRUE);
//Если флаг не установлен, пытаемся подключится к серверу
if(flag!=TRUE)
{
CPIP_clintDlg::CreateF();
};
szBuf=m_str.GetBuffer(m_str.GetLength());
//Передаем серверу введенную в форму строку
WriteFile(pipehandle, szBuf, strlen (szBuf) + 1, &cbWritten, NULL);
//Обнуляем форму
m_str="";
UpdateData(FALSE);
}"
Обработчик "READ":
"void CPIP_clintDlg::OnRead()
{
// Количество байт, принятых через канал
DWORD cbRead;
char szBuf[256];
char *temp;
//Если это первая попытка обращения к серверу, то пытаемся к нему подключится
if(flag!=TRUE)
{
CPIP_clintDlg::CreateF();
};
//Считываем данные
if(ReadFile(pipehandle, szBuf, 256,& cbRead, NULL))
{
//Выводим их пользователю
m_mess+=(CString)&szBuf[0];
m_mess += "\r\n";
UpdateData(FALSE);
}
//Обрабатываем ошибки
else
{
m_mess+="Ошибка функции Read File: Error";
itoa(GetLastError(), temp, 10 );
m_mess+=(CString)&temp[0];
UpdateData(FALSE);
}
}"

Вот, собственно, и все! Теперь вы имеете и клиента, и сервер, которые могут обращаться при помощи технологии PIPE-каналов. Компилируем обе программы. Запускаем сервер. Нажимаем "Read". При этом создается канал и начинается ожидание подключения клиент. Теперь запускаем клиента. Вводим какую-нибудь строку в форму SEND и нажимаем "SEND". При этом клиент подключается к серверу и отправляет ему введенную строку. Теперь и клиент, и сервер могут отправлять и читать сообщения друг друга. Если необходимо получить сообщение (и чтобы оно не удалялось из канала), используйте PeekNamed Pipe(). Хочется пожелать вам удачи в нелегкой жизни отечественных кодеров, а также семь гигабайт под программы ("


Компьютерная газета. Статья была опубликована в номере 12 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета