Delphi error io pending
Именованные каналы (Named Pipes) — это объекты ядра, являющиеся средством межпроцессной коммуникации между сервером канала и одним или несколькими клиентами канала.
Сервером канала называется процесс, создающий именованный канал.
Клиентом канала называется процесс, подключающийся к созданному именованному каналу.
От других аналогичных объектов именованные каналы отличает гарантированная доставка сообщений, возможность асинхронного ввода/вывода, возможность коммуникации между процессами на разных компьютерах в локальной вычислительной сети и относительная простота использования.
По своему назначению они похожи на каналы операционной системы UNIX.
При создании именованного канала ему назначается уникальное имя, определяется максимальное количество одновременных соединений с клиентами канала и режим работы канала (должен ли канал быть односторонним или двусторонним (дуплексным), ведется ли передача пакетами или потоком байтов. При передаче пакетами данные одной операции записи отделяются в буфере канала от данных другой операции записи).
Базовым объектом для реализации именованных каналов служит объект «файл», поэтому для посылки и приема сообщений по именованным каналам используются те же самые функции Windows API, что и при работы с файлами (ReadFile, WriteFile).
Для каждого процесса-клиента канала создается свой экземпляр канала, с собственными буферами и дескрипторами (handles) и с собственным механизмом передачи данных, не влияющим на остальные экземпляры.
Экземпляры одного канала имеют общее имя, указанное при создании, сервер назначает имя канала в соответствии с универсальными правилами именования (Universal Naming Convention, UNC), которые обеспечивают независимый от протоколов способ идентификации каналов в Windows-сетях [1].
Именованные каналы, также как и файлы, наследуют стандартную защиту объектов Windows, что позволяет разграничить участников коммуникации и обеспечить запрет несанкционированного доступа к каналу.
Создание именованных каналов возможно только в NT-системах, подключение к созданному каналу возможно как в NT-системах, так и в Win9x. Кроме того, API работы с каналами в Win9x не поддерживает асинхронных операций ввода/вывода.
Именованные каналы широко используются внутри самой системы. Например, взаимодействие менеджера сервисов с самими сервисами осуществляется через несколько именованных каналов. Для связи с сервисами RunAs, с планировщиком событий и с сервером локальной аутентификации также используются именованные каналы.
Именованные каналы являются наиболее простым способом организации связи между сервисами и пользовательскими приложениями, нуждающимися в такой связи.
Одним из полезных (и довольно уникальных) свойств именованного канала является возможность сервера заменять права своей учетной записи правами учетной записи клиента, соединившегося с каналом. Эта возможность служит преимущественно для ограничения прав сервера при выполнении операций доступа к различным объектам системы.
Для работы с именованными каналами Windows API предоставляет следующие функции:
CreateNamedPipe | Создание именованного канала или нового экземпляра канала. Функция доступна только серверу. |
ConnectNamedPipe или CreateFile | Подключение к экземпляру именованного канала со стороны клиента. Функция доступна только клиенту. |
WaitNamedPipe | Ожидание клиентом появления свободного экземпляра именованного канала для подключения к нему. |
ConnectNamedPipe | Ожидание сервером подключения клиента к экземпляру именованного канала. |
ReadFile, ReadFileEx | Чтение данных из именованного канала. Функция доступна как клиенту, так и серверу. |
WriteFile, WriteFileEx | Запись данных в именованный канал. Функция доступна как клиенту, так и серверу. |
PeekNamedPipe | Чтение данных из именованного канала без удаления прочитанных данных из буфера канала. Функция доступна как клиенту, так и серверу. |
TransactNamedPipe | Запись и чтение из именованного канала одной операцией. Функция доступна как клиенту, так и серверу. |
DisconnectNamedPipe | Отсоединение сервера от экземпляра именованного канала. |
GetNamedPipeInfo | Получение информации об именованном канале. |
GetNamedPipeHandleState | Получение текущего режима работы именованного канала и количества созданных экземпляров канала. |
SetNamedPipeHandleState | Установка текущего режима работы именованного канала. |
CloseHandle | Закрытие дескриптора экземпляра именованного канала, освобождение связанных с объектом ресурсов. |
FlushFileBuffers | Сброс данных из кэша в буфер канала. |
Рассмотрим алгоритм работы сервера двустороннего именованного канала, обслуживающего одновременно нескольких клиентов с использованием асинхронного ввода-вывода. Работа данного сервера заключается в пересылке сообщений, полученных от каждого клиента всем клиентам, подключенным к каналу (некий аналог чата).
После инициализации, процесс сервера запускает поток, обслуживающий клиентское соединение. Поток создает именованный канал, используя функцию CreateNamedPipe, указывает максимальное количество экземпляров канала (равное максимально возможному количеству одновременно обслуживаемых клиентов) и режимы работы канала, одновременно с созданием канала создается первый его экземпляр.
Затем поток сервера, обслуживающий этот экземпляр именованного канала, вызывает функцию ConnectNamedPipe для ожидания клиентского подключения.
После того, как клиент подключился к экземпляру именованного канала, сервер инициализирует структуры для обмена данными с клиентом, читает из канала имя подсоединившегося клиента, регистрирует его и запускает копию этого же потока для обслуживания следующего клиентского подключения. Далее, поток обслуживания экземпляра канала входит в цикл чтения канала и рассылки прочитанных сообщений всем подключенным клиентам до тех пор, пока соединение с клиентом не разорвется.
После разрыва соединения поток отключается от экземпляра канала, чтобы освободить ресурсы, выделенные системой под обслуживание этого экземпляра, и завершает свою работу.
Последующие потоки при вызове функции CreateNamedPipe создают дополнительные экземпляры канала, при этом параметры, задающие режимы работы канала функцией игнорируются.
Алгоритм работы клиента следующий: После запуска клиент запрашивает у пользователя имя сервера и свое имя для идентификации, подключается к экземпляру канала вызовом функции CreateFile с указанием имени канала, регистрируется на сервере с помощью посылки в канал своего имени и запускает поток асинхронного чтения сообщений из канала. Основной поток принимает сообщения пользователя и передает их в канал серверу. Поток чтения работает до тех пор, пока не разорвется соединение с сервером, или пока пользователь не завершит клиентский процесс.
Рассмотрим реализацию потока сервера, обслуживающего экземпляр именованного канала:
и процедуры рассылки сообщения клиентам: Рассмотрим реализацию клиента, взаимодействующего с сервером именованного канала: И клиентского потока асинхронного чтения данных из канала:
Полный текст примера сервера и клиента, использующих именованный канал для коммуникации можно найти в приложении к статье.
Как уже отмечалось ранее, базовым объектом для реализации именованных каналов является объект «Файл». Это позволяет перечислить созданные в системе именованные каналы программно средствами Native API: открыть корневой каталог файловой системы именованных каналов (DeviceNamedPipe) и перечислить его содержимое. Пример программы перечисления созданных именованных каналов можно найти на сайте http://www.sysinternals.com (PipeList) или в приложении к статье.
Автор выражает признательность Екатерине Субботиной, Дмитрию Заварзину и Александру Жигалину за конструктивную критику в процессе написания статьи.
Источник
Функция ReadFile
Функция ReadFile читает данные из файла, начиная с позиции, обозначенной указателем файла. После того, как операция чтения была закончена, указатель файла перемещается на число действительно прочитанных байтов, если дескриптор файла не создан с атрибутом асинхронной операции. Если дескриптор файла создается для асинхронного ввода — вывода, приложение должно переместить позицию указателя файла после операции чтения.
Эта функция предназначена и для синхронной и асинхронной операции. Функция ReadFileEx предназначена исключительно для асинхронной операции. Это дает возможность приложению выполнять другие действия в ходе операции чтения файла.
[in] Дескриптор файла, который читается. Дескриптор файла должен быть, создан с правом доступа GENERIC_READ . Подробную информацию см. в статье Защита файла и права доступа.
Windows NT/2000/XP: Для асинхронных операций чтения, параметр hFile может быть любым дескриптором открытым с флажком FILE_FLAG_OVERLAPPED функцией CreateFile или дескриптор сокета, возвращенный функцией socket или accept.
Windows 95/98/Me: Для асинхронных операций чтения, параметр hFile может быть коммуникационным ресурсом, открытым с флажком FILE_FLAG_OVERLAPPED функцией CreateFile или дескриптор сокета, возвращенный при помощи функций socket или accept . Вы не можете выполнять асинхронные операции чтения в почтовом ящике в ядре Windows, именованных каналах или файлах на диске.
[out] Указатель на буфер, который принимает прочитанные данные из файла.
[in] Число байтов, которые читаются из файла.
[out] Указатель на переменную, которая получает число прочитанных байтов. Функция ReadFile устанавливает это значение в нуль перед началом любой работы или проверкой ошибок. Если этот параметр равняется нулю, когда ReadFile возвращает значение ИСТИНА (TRUE) для именованного канала, другой конец канала в режиме передачи сообщений вызывает функцию WriteFile с параметром nNumberOfBytesToWrite установленным в нуль.
Windows NT/2000/XP: Если параметр lpOverlapped имеет значение ПУСТО (NULL), параметр lpNumberOfBytesRead не может быть значением ПУСТО (NULL). Если lpOverlapped имеет — значение не ПУСТО (NULL), lpNumberOfBytesRead может быть значением ПУСТО (NULL). Если это — асинхронная операция чтения, Вы можете получить число прочитанных байтов при помощи вызова функции GetOverlappedResult . Если параметр hFile связан с портом завершения ввода-вывода, Вы можете получить число прочитанных байтов при помощи вызова функции GetQueuedCompletionStatus .
Если используются порты завершения ввода-вывода, а Вы используете процедуру повторного вызова, чтобы освободить занимаемую память структурой OVERLAPPED , на которую указывает параметр lpOverlapped, задайте ПУСТО (NULL), как значение этого параметра, чтобы избежать проблемы искажения данных в памяти в ходе ее освобождения. Эта проблема искажения данных в памяти становится причиной возвращения в этом параметре неверного числа байтов .
Windows 95/98/Me: Этот параметр не может быть ПУСТО (NULL).
[in] Указатель на структуру OVERLAPPED. Эта структура требуется тогда, если параметр hFile создавался с флажком FILE_FLAG_OVERLAPPED .
Если hFile был открыт с флажком FILE_FLAG_OVERLAPPED , у параметра lpOverlapped не должно быть значения ПУСТО (NULL). Он должен указать на правильную структуру OVERLAPPED. Если hFile создавался с флажком FILE_FLAG_OVERLAPPED , а lpOverlapped имеет значение ПУСТО (NULL), функция может неправильно сообщить о завершении операций чтения.
Если hFile был открыт с флажком FILE_FLAG_OVERLAPPED , а lpOverlapped имеет значение не ПУСТО (NULL), операция чтения начинается при смещении, заданном в структуре OVERLAPPED, и ReadFile может возвратить значение прежде, чем операция чтения будет закончена. В этом случае, ReadFile возвращает значение ЛОЖЬ (FALSE), а функция GetLastError возвращает значение ERROR_IO_PENDING . Это дает возможность вызывающему процессу продолжиться, в то время как операция чтения заканчивается. Событие, определяемое в структуре OVERLAPPED устанавливается в сигнальное состояние после завершения операции чтения. Вызывающая программа должна корректировать местоположение указателя позиции в файле после завершения работы.
Функция ReadFile сбрасывает событие, указанное членом hEvent структуры OVERLAPPED в несигнальное состояние, когда она начинает операцию ввода-вывода (I/O). Поэтому, нет необходимости для вызывающей программы, чтобы делать так.
Если hFile не открывался с флажком FILE_FLAG_OVERLAPPED , а lpOverlapped — значение ПУСТО (NULL), операции чтения начинается в текущей позиции файла и ReadFile не возвращает значения до тех пор, пока операция не будет закончена. Система модернизирует указатель позиции в файле после завершения работы.
Если hFile не открывался с FILE_FLAG_OVERLAPPED , а lpOverlapped — не ПУСТО (NULL), операция чтения стартует при смещении, указанном в структуре OVERLAPPED. ReadFile не возвращает значение до тех пор, пока операция чтения не завершилась. Система модернизирует позицию указателя в файле после завершения работы.
Windows 95/98/Me: Для операций с файлами, дисками, каналами или почтовыми ящиками в ядре Windows, этим параметром должно быть значение ПУСТО (NULL); указатель на структуру OVERLAPPED порождает вызов, который завершиться ошибкой. Тем не менее, Windows 95/98/Me поддерживает асинхронные операции на последовательных и параллельных портах.
Функция ReadFile возвращает значение тогда, когда выполнено одно из ниже перечисленных условий:
— операция записи завершается на записывающем конце канала,
— затребованное число байтов прочитано,
— или происходит ошибка.
Если функция завершается успешно, величина возвращаемого значения — не ноль.
Если функция завершается с ошибкой, величина возвращаемого значения — ноль. Чтобы получить дополнительные сведения об ошибке, вызовите GetLastError.
Если величина возвращаемого значения — не ноль, а число прочитанных байтов равняется нулю, указатель файла был за пределами текущего конца файла на момент операции чтения. Однако, если файл был открыт с флажком FILE_FLAG_OVERLAPPED , и lpOverlapped имеет значение не ПУСТО (NULL), величина возвращаемого значения — ноль, а GetLastError возвращает ошибку ERROR_HANDLE_EOF , когда указатель файла проходит вне текущего конца файла.
Если часть файла блокирована другим процессом, а операция чтения накладывается на блокированную часть, эта функция завершается ошибкой.
Когда работа с файлами открывается с флажком FILE_FLAG_NO_BUFFERING , приложение должно соответствовать некоторым требованиям:
- Доступ к файлу должен начаться при смещениях байтов в пределах файла, которые должны быть целыми числами кратными размеру сектора тома. Чтобы выяснить размер сектора тома, вызовите функцию GetDiskFreeSpace .
- Доступ к файлу должен быть числом байтов, которое является целым числом кратным размеру сектора тома. Например, если размер сектора — 512 байтов, приложение может запросить для чтения и записи 512, 1024 или 2048 байтов, но не 335, 981 или 7171 байт.
- Адреса буферов для операций чтения и записи должны выравниваться кратно сектору (выравниваются адреса в памяти, которые являются целыми числами, кратными размеру сектора тома). Один из способов выравнивания буфера кратно сектору, является использование функции VirtualAlloc , которая назначает буфера. Эта функция выделяет память, выравнивая их по адресам, которые являются целыми числами, кратными размеру страницы памяти системы. Поскольку размеры и страницы памяти, и сектора тома являются степенью 2, память, выровненная кратно размеру страницы памяти системы также выравнивается кратно и размеру сектора тома.
Доступ к буферу ввода, в то время как операция чтения использует этот буфер, может привести к искажению прочитанных данных в буфер. Приложения не должны читать из, записывать в, перераспределять или освобождать буфер ввода, который использует операция чтения до тех пор, пока она не завершится.
Символы могут читаться из буфера ввода консоли при помощи использования функции ReadFile с дескриптором консольного ввода данных. Консольный режим работы обуславливает правильное поведение функции ReadFile.
Если именованный канал читается в режиме передачи сообщений, а длина следующего сообщения больше, чем это устанавливается параметром nNumberOfBytesToRead, ReadFile возвращает значение ЛОЖЬ (FALSE), а GetLastError возвращает ошибку ERROR_MORE_DATA . Остаток сообщения может быть прочитан последующим вызовом функции ReadFile или PeekNamedPipe .
При чтении в устройстве обмена информацией (коммуникационном), поведение ReadFile управляется текущими коммуникационными задержками как установлено и возвращено обратно использованием функций SetCommTimeouts и GetCommTimeouts. Непредсказуемые результаты могут произойти, если Вы будете не в состоянии установить значения времени простоя. Для получения дополнительной информации о коммуникационных блокировках по времени, см. статью о структуре COMMTIMEOUTS.
Если ReadFile пытается читать из почтового слота, буфер которого является слишком маленьким, функция возвращает значение ЛОЖЬ (FALSE), а GetLastError возвращает ошибку ERROR_INSUFFICIENT_BUFFER .
Если дескриптор анонимного канала записи был закрыт, а функция ReadFile пытается читать используя дескриптор соответствующего анонимного канала чтения, функция возвращает значение ЛОЖЬ (FALSE), а GetLastError возвращает ошибку ERROR_BROKEN_PIPE .
Функция ReadFile может завершиться ошибкой и возвратить значение ERROR_INVALID_USER_BUFFER или ERROR_NOT_ENOUGH_MEMORY всякий раз, когда имеется слишком много ожидающих обработки асинхронных запросов ввода — вывода.
Код проверки ReadFile положения метки конца файла (eof) различается для синхронных и асинхронных операций чтения.
Когда синхронная операция чтения достигает конца файла, ReadFile возвращает значение ИСТИНА (TRUE) и устанавливает в параметре *lpNumberOfBytesRead нуль. Нижеследующий типовой код проверяет метку конца файла для синхронной операции чтения:
Асинхронная операция чтения может встретиться с концом файла в течение инициализации вызова ReadFile, или в ходе последующей асинхронной операции.
Если EOF обнаруживается функцией ReadFile во время асинхронной операции чтения, ReadFile возвращает значение ЛОЖЬ (FALSE), а GetLastError возвращает ошибку ERROR_HANDLE_EOF .
Если EOF обнаруживается в ходе последующей асинхронной операции, происходит вызов функции GetOverlappedResult , чтобы получить в результате этой операции возвращенное значение ЛОЖЬ (FALSE), а GetLastError возвратит ошибку ERROR_HANDLE_EOF .
Чтобы отменить все ждущие обработки асинхронные операции ввода-вывода (I/O), используйте функцию CancelIo . Эта функция отменяет только операции, порождаемые вызывающим потоком для заданного дескриптора файла. Отмененные операции ввода-вывода (I/O) завершаются ошибкой ERROR_OPERATION_ABORTED .
Если Вы пытаетесь читать из накопителя на гибких дисках, который не имеет дискеты, система показывает на экране окно сообщения, предлагающее пользователю повторить операцию. Чтобы воспрепятствовать системе показывать это окно, вызовите функцию SetErrorMode с флажком SEM_NOOPENFILEERRORBOX .
Нижеследующий типовой код иллюстрирует проверку на метку конца файла для асинхронной операции чтения:
Размещение и совместимость ReadFile
Источник
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
procedure writetofile(const str: string); var FileHandle: THandle; OverLap: OVERLAPPED; StringToWrite : string; Size: Integer; LastError, Written: DWORD; begin ZeroMemory(@OverLap,sizeof(OverLap)); StringToWrite := str; Size := Length(StringToWrite) * SizeOf(Char); // Юникодные версии Дельфи FileHandle := CreateFile('F:Protocol.txt', GENERIC_WRITE, 0, nil, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, 0); if FileHandle <> INVALID_HANDLE_VALUE then // Файл создан? begin // Да, все нормально, продолжаем. Создаем Event и сбрасываем его Overlap.hEvent := CreateEvent(nil, True, False, nil); ResetEvent(OverLap.hEvent); // Тут у нас запись началась в любом случае, ведь WriteFile вызывается. // Ошибки будем обрабатывать позже Form1.Memo3.Lines.Add('Запись началась'); // Теперь о вызове WriteFile. Внимательно смотрим на второй параметр. Так передаются строки if not WriteFile(FileHandle, StringToWrite[1], Size, Written, @Overlap) then begin // Обработка ошибок. Если WriteFile вернула False, но при этом GetLastError = ERROR_IO_PENDING // Значит, это была не ошибка, и надо дождаться окончания записи LastError := GetLastError; if LastError = ERROR_IO_PENDING then while WaitForSingleObject(Overlap.hEvent, 1000) = WAIT_TIMEOUT do Application.ProcessMessages else // А вот если не ERROR_IO_PENDING - то была ошибка, рассказываем пользователю об этом Form1.Memo3.Lines.Add(Format('WriteFile. Ошибка: Error = %s', [SysErrorMessage(GetLastError)])) (* ShowMessage(Format('WriteFile. Ошибка: Error = %s', [SysErrorMessage(GetLastError)])); *) end; // Эта часть вообще не нужна. Если WriteFile вернула True - то всё в порядке (* else ShowMessage(Format('Запись потерпела неудачу : Error = %s', [SysErrorMessage(GetLastError)])); *) // А вот теперь смотрим, что даёт GetOverlappedResult if not GetOverlappedResult(FileHandle, Overlap, Written, True) then begin Form1.Memo3.Lines.Add(Format('GetOverlappedResult. Запись потерпела неудачу : Error = %s', [SysErrorMessage(GetLastError)])); (* ShowMessage(Format('GetOverlappedResult. Запись потерпела неудачу : Error = %s', [SysErrorMessage(GetLastError)])); *) end else Form1.Memo3.Lines.Add(Format('Запись закончена! Записано байт %d', [Written])); // ShowMessage(Format('Запись закончена! Записано байт %d', [Written])); CloseHandle(Overlap.hEvent); CloseHandle(FileHandle); end else Form1.Memo3.Lines.Add('Не удалось открыть файл'); // ShowMessage('Не удалось открыть файл'); end; procedure TForm1.Button1Click(Sender: TObject); begin writetofile('Just a test'); end; |
Перед разработчиком приложений Windows нередко встаёт вопрос, как обмениваться данными между двумя и более приложениями. Это бывает необходимо, например, если вы разрабатываете службу Windows и приложение для настройки и мониторинга за этой службой.
Для обмена данными между приложениями Windows существует много механизмов: буфер обмена (Clipboard), технология COM, передача данных между окнами процессов с помощью сообщения WM_COPYDATA, удаленный вызов процедур (RPC), сокеты (Windows Sockets), динамический обмен данными (DDE), именованные каналы (Named Pipes), почтовый ящик (Mailslot) и проецируемые в память файлы (Mapped file). У каждого способа есть свои плюсы и минусы. Здесь я не буду их рассматривать.
В данной статье, я хотел бы рассмотреть только один из перечисленных механизмов – именованные каналы (Named Pipes). Вообще существует два вида каналов: именованные и анонимные. Анонимные каналы обычно создаются родительским процессом и передаются дочернему процессу при его создании. У именованных каналов есть имя и любой процесс, знающий это имя, может подключиться к каналу. В любом случае, при работе с каналами у вас есть сервер (приложение создающее канал) и клиенты (приложения, подключающиеся к каналу).
Для работы с каналами в Windows есть системные функции, такие как CreateNamedPipe, TransactNamedPipe и другие. Но реализация Pipe-сервера и Pipe-клиента не лёгкая задача, поэтому я отправился на поиски готовых реализаций в Интернет.
Демонстрационные классы FWIOCompletionPipe
Первое, что я нашел решение FWIOCompletionPipes. Собственно это демонстрационная реализация сервера и клиента. В архиве вы найдёте юнит FWIOCompletionPipes.pas и примеры. В юните FWIOCompletionPipes.pas есть класс для создания сервера TFWPipeServer и класс клиента TFWPipeClient. Сразу скажу, что работу сервера я не проверял, а клиент пригодился, но об этом ниже.
Юнит Pipes
Отличная реализация Pipe-клиента и Pipe-сервера попалась мне на блоге Mick’s Mix. Чтобы код заработал под Delphi XE3 без ошибок и предупреждений, пришлось кое-что поправить. Мой исправленный вариант вы можете скачать здесь:
Файлы:
Есть вариант юнита c поддержкой всех версий Delphi и платформы Win64:
В юните есть класс-сервер TPipeServer, класс-клиент TPipeClient и даже есть приятный бонус — класс для запуска и управления консольным приложением с перехватом потока вывода TPipeConsole. Юнит лучше всего подключить к новому bpl-проекту и инсталлировать в среде Delphi. Тогда компоненты будут доступны в окне инструментов Tool Palette.
Пользоваться компонентами одно удовольствие. Чтобы создать сервер, создайте экземпляр класса TPipeServer (или положите его на форму или модуль данных), задайте имя канала, установите свойство Active в true и обрабатывайте событие OnPipeMessage. Пример обработки полученного сообщения (здесь и ниже будем считать, что в качестве сообщения мы передаём и получаем XML-документ):
procedure TForm1.PipeServer1PipeMessage(Sender: TObject; Pipe: NativeUInt; Stream: TStream); var msg: IXMLDocument; begin msg := TXMLDocument.Create(nil); //Загружаем XML-документ из потока. msg.LoadFromStream(Stream); //Отображаем полученный XML-документ. ShowMessage(msg.XML.Text); end;
Теперь, чтобы отправить сообщение серверу нужно воспользоваться функцией SendStream:
procedure TForm2.Button1Click(Sender: TObject); var xml: IXMLDocument; memoryStream: TMemoryStream; begin memoryStream := TMemoryStream.Create; try //Создаём XML-документ для отправки. xml := TXMLDocument.Create(nil); xml.LoadFromXML('<MyMessage />'); //Сохраняем XML-документ в поток. xml.SaveToStream(memoryStream); //Подключаемся к Pipe-серверу. PipeClient1.Connect; //Отправляем данные серверу. PipeClient1.SendStream(memoryStream); finally memoryStream.Free; end; end;
Теперь разберёмся, как ответить клиенту на сообщение. Это можно сделать так:
procedure TForm1.PipeServer1PipeMessage(Sender: TObject; Pipe: NativeUInt; Stream: TStream); var xml: IXMLDocument; begin xml := TXMLDocument.Create(nil); //Загружаем XML-документ из потока. xml.LoadFromStream(Stream); //Отображаем полученный XML-документ. ShowMessage(xml.XML.Text); //Отправляем клиенту полученное сообщение обратно. TPipeServer(Sender).SendStream(Pipe, Stream); end;
Если нам нужно отправить сообщение всем клиентам (сделать рассылку), это можно сделать так:
procedure TForm1.Button1Click(Sender: TObject); var i: Integer; xml: IXMLDocument; memoryStream: TMemoryStream; begin memoryStream := TMemoryStream.Create; try //Создаём XML-документ для отправки. xml := TXMLDocument.Create(nil); xml.LoadFromXML('<MyMessage />'); //Сохраняем XML-документ в поток. xml.SaveToStream(memoryStream); //Рассылаем сообщение всем клиентам. for i := 0 to PipeServer1.ClientCount - 1 do PipeServer1.SendStream(PipeServer1.Clients[i], memoryStream); finally memoryStream.Free; end; end;
При всём удобстве компонентов есть одна загвоздка. Если с клиента вам нужно отправить сообщение серверу и сразу синхронно получить ответ, то такой режим работы не поддерживается. Немножко поискав, я нашел решение для консольного приложения:
program CmdClient; {$APPTYPE CONSOLE} uses Windows, Messages, SysUtils, Pipes; type TPipeEventHandler = class(TObject) public procedure OnPipeSent(Sender: TObject; Pipe: HPIPE; Size: DWORD); end; procedure TPipeEventHandler.OnPipeSent(Sender: TObject; Pipe: HPIPE; Size: DWORD); begin WriteLn('On Pipe Sent has executed!'); end; var lpMsg: TMsg; WideChars: Array [0..255] of WideChar; myString: String; iLength: Integer; pcHandler: TPipeClient; peHandler: TPipeEventHandler; begin // Create message queue for application PeekMessage(lpMsg, 0, WM_USER, WM_USER, PM_NOREMOVE); // Create client pipe handler pcHandler:=TPipeClient.CreateUnowned; // Resource protection try // Create event handler peHandler:=TPipeEventHandler.Create; // Resource protection try // Setup clien pipe pcHandler.PipeName:='myNamedPipe'; pcHandler.ServerName:='.'; pcHandler.OnPipeSent:=peHandler.OnPipeSent; // Resource protection try // Connect if pcHandler.Connect(5000) then begin // Dispatch messages for pipe client while PeekMessage(lpMsg, 0, 0, 0, PM_REMOVE) do DispatchMessage(lpMsg); // Setup for send myString:='the message I am sending'; iLength:=Length(myString) + 1; StringToWideChar(myString, wideChars, iLength); // Send pipe message if pcHandler.Write(wideChars, iLength * 2) then begin // Flush the pipe buffers pcHandler.FlushPipeBuffers; // Get the message if GetMessage(lpMsg, pcHandler.WindowHandle, 0, 0) then DispatchMessage(lpMsg); end; end else // Failed to connect WriteLn('Failed to connect to ', pcHandler.PipeName); finally // Show complete Write('Complete...'); // Delay ReadLn; end; finally // Disconnect event handler pcHandler.OnPipeSent:=nil; // Free event handler peHandler.Free; end; finally // Free pipe client pcHandler.Free; end; end.
Но этот случай не подходит для оконного приложения. Здесь мне пришлось изобретать велосипед, чтобы получить ответ от сервера синхронно. Я использовал компонент TFWPipeClient, что описан выше, чтобы отправить сообщение и получить ответ. Но выяснилось, что компонент TPipeServer отправляет клиенту не просто данные, а так называемый пакет данных. Т.е. сначала он отправляет клиенту маркер начала пакета, затем сами данные (т.к. если объём данных большой, то данные разбиваются на куски) и, в конце, маркер конца пакета. Также мне понадобился хэндл канала открытого компонентом TFWPipeClient поэтому пришлось добавить свойство Pipe. Мой дополненный вариант юнита FWIOCompletionPipes.pas можете скачать здесь:
В итоге, синхронная отправка сообщения серверу и получение ответа выглядит так:
//Функция отправляет сообщение серверу и ждёт ответа, если параметр waitForAnswer выставлен в true. function TForm2.SendMessageToServer(Message: IXMLDocument; waitForAnswer: boolean): IXMLDocument; var inputStream, outputStream: TMemoryStream; fwPipeClient: TFWPipeClient; buff: array [0..MAXWORD - 1] of Char; lpBytesRead: DWORD; //Функция проверки, являются ли полученные данные маркерами начала и конца пакета. function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean; var lpControlMsg : PPipeMsgBlock; begin result := false; if size = SizeOf(TPipeMsgBlock) then begin lpControlMsg := PPipeMsgBlock(buffer); if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock)) and (lpControlMsg^.MagicStart = MB_MAGIC) and (lpControlMsg^.MagicEnd = MB_MAGIC) and (lpControlMsg^.ControlCode = controlCode) then result := true; end; end; begin result := nil; inputStream := TMemoryStream.Create; try Message.SaveToStream(inputStream); if not waitForAnswer then //Если не нужно ожидать ответа, то отправляем сообщение как обычно. PipeClient1.SendStream(inputStream) else begin //Создаём альтернативный Pipe-клиент. fwPipeClient := TFWPipeClient.Create('.', PipeClient1.PipeName); try //Подключаем Pipe-клиент к серверу. fwPipeClient.Active := true; outputStream := TMemoryStream.Create; try //Отправляем данные серверу. fwPipeClient.SendData(inputStream, outputStream); //Если сервер вернул маркер начала пакета, то будем считывать весь пакет. if IsPacketBound(outputStream.Memory, outputStream.Size, MB_START) then begin outputStream.Size := 0; while True do begin if ReadFile(fwPipeClient.Pipe, buff[0], MAXWORD, lpBytesRead, nil) then begin if IsPacketBound(@buff[0], lpBytesRead, MB_END) then break else outputStream.Write(buff, lpBytesRead); end; end; end; result := TXMLDocument.Create(nil); result.LoadFromStream(outputStream); finally outputStream.Free; end; finally fwPipeClient.Free; end; end; finally inputStream.Free; end; end;
А вот пример использования функции SendMessageToServer:
procedure TForm2.Button2Click(Sender: TObject); var msg, answer: IXMLDocument; begin //Создаём XML-документ для отправки. msg := TXMLDocument.Create(nil); msg.LoadFromXML('<MyMessage />'); //Отправляем сообщение и получаем ответ. answer := SendMessageToServer(msg, true); //Отображаем полученное от сервера сообщение. ShowMessage(answer.XML.Text); end;
Позже, при тестировании, выяснилось, что в классе TFWPipeClient неправильно реализовано подключение к каналу, поэтому при активной работе с каналом часто возникает ошибка System Error. Code: 231. Все копии канала заняты. Поэтому мне пришлось переписать функцию SendMessageToServer отказавшись от использования класса TFWPipeClient:
//Функция отправляет сообщение серверу и ждёт ответа, если параметр waitForAnswer выставлен в true. function TForm2.SendMessageToServer(Message: IXMLDocument; waitForAnswer: boolean): IXMLDocument; var inputStream, outputStream: TMemoryStream; buff: array [0..MAXWORD - 1] of Char; lpBytesRead: DWORD; lpNumberOfBytesWritten: DWORD; lpMode: DWORD; pipe: THandle; //Функция проверки, являются ли полученные данные маркерами начала и конца пакета. function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean; var lpControlMsg : PPipeMsgBlock; begin result := false; if size = SizeOf(TPipeMsgBlock) then begin lpControlMsg := PPipeMsgBlock(buffer); if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock)) and (lpControlMsg^.MagicStart = MB_MAGIC) and (lpControlMsg^.MagicEnd = MB_MAGIC) and (lpControlMsg^.ControlCode = controlCode) then result := true; end; end; begin result := nil; inputStream := TMemoryStream.Create; try Message.SaveToStream(inputStream); if not waitForAnswer then //Если не нужно ожидать ответа, то отправляем сообщение как обычно. PipeClient1.SendStream(inputStream) else begin pipe := INVALID_HANDLE_VALUE; try //Подключаемся к каналу. while True do begin pipe := CreateFile(PChar('\.pipe' + PipeClient1.PipeName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0); if pipe <> INVALID_HANDLE_VALUE then break; if GetLastError <> ERROR_PIPE_BUSY then RaiseLastOSError; Win32Check(WaitNamedPipe(PChar('\.pipe' + PipeClient1.PipeName), NMPWAIT_WAIT_FOREVER)); end; lpMode := PIPE_READMODE_MESSAGE; Win32Check(SetNamedPipeHandleState(pipe, lpMode, nil, nil)); //Отправляем данные серверу. Win32Check(WriteFile(pipe, inputStream.Memory^, inputStream.Size, lpNumberOfBytesWritten, nil)); outputStream := TMemoryStream.Create; try Win32Check(ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, nil)); outputStream.Write(buff, lpBytesRead); //Если сервер вернул маркер начала пакета, то будем считывать весь пакет. if IsPacketBound(outputStream.Memory, outputStream.Size, MB_START) then begin outputStream.Size := 0; while True do begin if ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, nil) then begin if IsPacketBound(@buff[0], lpBytesRead, MB_END) then break else outputStream.Write(buff, lpBytesRead); end; end; end; result := TXMLDocument.Create(nil); result.LoadFromStream(outputStream); finally outputStream.Free; end; finally if pipe <> INVALID_HANDLE_VALUE then CloseHandle(pipe); end; end; finally inputStream.Free; end; end;
Добавлено 29.07.2016. Но функция, приведённая выше не совершенна, и если наш сервер упал и не отвечает, то программа в ней зависнет и будет до бесконечности ждать, пока сервер не пришлёт ответ. Это можно исправить, если ввести таймауты для каждой отдельной операции при работе с каналом (подключение к каналу, чтение из него и запись в него). В этом случае функция существенно усложнится. Вот что получится:
//Функция отправляет сообщение серверу и ждёт ответа указанное в параметре timeout время (в мс), если параметр waitForAnswer выставлен в true. function TForm2.SendMessageToServer(Message: IXMLDocument; waitForAnswer: boolean; timeout: DWORD): IXMLDocument; var inputStream, outputStream: TMemoryStream; buff: array [0..MAXWORD - 1] of Char; lpBytesRead: DWORD; lpNumberOfBytesWritten: DWORD; lpMode: DWORD; pipe: THandle; isPacket: boolean; pendingRead: boolean; lastError: integer; olapRead: TOverlapped; olapWrite: TOverlapped; events: array [0 .. 0] of THandle; function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean; var lpControlMsg : PPipeMsgBlock; begin result := false; if size = SizeOf(TPipeMsgBlock) then begin lpControlMsg := PPipeMsgBlock(buffer); if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock)) and (lpControlMsg^.MagicStart = MB_MAGIC) and (lpControlMsg^.MagicEnd = MB_MAGIC) and (lpControlMsg^.ControlCode = controlCode) then result := true; end; end; function AddPortion: boolean; begin result := false; if (not isPacket) and IsPacketBound(@buff[0], lpBytesRead, MB_START) then isPacket := true else if (not isPacket) then begin outputStream.Write(buff, lpBytesRead); result := true; end else if isPacket then begin if IsPacketBound(@buff[0], lpBytesRead, MB_END) then begin isPacket := false; result := true; end else outputStream.Write(buff, lpBytesRead); end; end; procedure ReadMessage(timeout: integer; generateTimeoutError: boolean); begin while true do begin pendingRead := false; if ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, @olapRead) then begin if AddPortion then exit; ResetEvent(olapRead.hEvent); end else begin lastError := GetLastError; if lastError = ERROR_IO_PENDING then pendingRead := true else if (lastError = ERROR_MORE_DATA) then begin if AddPortion then exit; end else RaiseLastOSError(lastError); end; if pendingRead then begin events[0] := olapRead.hEvent; case WaitForMultipleObjects(1, @events, false, timeout) of WAIT_OBJECT_0: begin if GetOverlappedResult(pipe, olapRead, lpBytesRead, true) then begin if AddPortion then exit; end else begin lastError := GetLastError; if lastError = ERROR_MORE_DATA then begin if AddPortion then exit; end end; end; WAIT_TIMEOUT: if generateTimeoutError then raise Exception.Create('За отведённое время не получен ответ.') else break; else RaiseLastOSError; end; end; end; end; begin isPacket := false; pipe := INVALID_HANDLE_VALUE; pendingRead := false; ClearOverlapped(olapRead, True); ClearOverlapped(olapWrite, True); result := nil; inputStream := TMemoryStream.Create; try Message.SaveToStream(inputStream); if not waitForAnswer then PipeClient1.SendStream(inputStream) else begin try olapRead.hEvent := CreateEvent(nil, True, False, nil); olapWrite.hEvent := CreateEvent(nil, True, False, nil); try outputStream := TMemoryStream.Create; try while True do begin pipe := CreateFile(PChar('\.pipe' + PipeClient1.PipeName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0); if pipe <> INVALID_HANDLE_VALUE then break; if GetLastError <> ERROR_PIPE_BUSY then RaiseLastOSError; Win32Check(WaitNamedPipe(PChar('\.pipe' + PipeClient1.PipeName), timeout)); end; lpMode := PIPE_READMODE_MESSAGE; Win32Check(SetNamedPipeHandleState(pipe, lpMode, nil, nil)); while PeekNamedPipe(pipe, nil, 0, nil, @lpBytesRead, nil) and (lpBytesRead > 0) do begin ReadMessage(timeout, false); outputStream.Size := 0; end; if WriteFile(pipe, inputStream.Memory^, inputStream.Size, lpNumberOfBytesWritten, @olapWrite) then begin FlushFileBuffers(pipe); ResetEvent(olapWrite.hEvent); end else begin lastError := GetLastError; if lastError = ERROR_IO_PENDING then begin events[0] := olapWrite.hEvent; case WaitForMultipleObjects(1, @events, false, timeout) of WAIT_OBJECT_0: begin if GetOverlappedResult(pipe, olapWrite, lpNumberOfBytesWritten, True) then FlushFileBuffers(pipe) else RaiseLastOSError; end; WAIT_TIMEOUT: raise Exception.Create('За отведённое время не удалось отправить сообщение.'); else RaiseLastOSError; end; end else RaiseLastOSError(lastError); end; ReadMessage(timeout, true); result := TXMLDocument.Create(nil); result.LoadFromStream(outputStream); finally outputStream.Free; end; finally CloseHandle(olapRead.hEvent); CloseHandle(olapWrite.hEvent); end; finally if pipe <> INVALID_HANDLE_VALUE then CloseHandle(pipe); end; end; finally inputStream.Free; end; end;
Подведём итог. Используя вышеописанное решение, вы можете быстро наладить взаимодействие между несколькими процессами на одном компьютере под управлением Windows. Вы сможете отправлять большие объёмы информации и реализовать синхронное и асинхронное взаимодействие.
Вызов FileRead в асинхронном режиме
, чтение файла в асинхронном режиме
- Подписаться на тему
- Сообщить другу
- Скачать/распечатать тему
|
|
Full Member Рейтинг (т): 14 |
решил разобраться с вызовом функций WinApi в асинхронном режиме. procedure TForm1.DoAsyncLoad(); var lReaded: Cardinal; lTransferred: Cardinal; lBuff: Pointer; lRes: Boolean; lErr, lErr2: Cardinal; lFileHandle: Cardinal; lOverlapped: TOverlapped; begin lFileHandle := CreateFile( PChar( edtFileName.Text ), GENERIC_READ, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED, 0 ); if lFileHandle = INVALID_HANDLE_VALUE then RaiseLastOSError; try lReaded := 0; ZeroMemory( @lOverlapped, SizeOf( TOverlapped )); GetMem( lBuff, C_BLOCK_SIZE ); //используем большой буфер try repeat lReaded := 0; //обнуляем внутренние данные для нового вызова «перекрываемой» функции lOverlapped.Internal := 0; lOverlapped.InternalHigh := 0; //hEvent не задаю, так как нигде ожидать ничего не будем lRes := ReadFile( lFileHandle, lBuff^, C_BLOCK_SIZE, lReaded, @lOverlapped ); if not lRes then lErr := GetLastError else lErr := ERROR_SUCCESS; if not lRes then begin //обрабатываем случаи, когда ReadFile = False case lErr of //проверка ощибки ReadFile ERROR_IO_PENDING: //асинхронная операция begin lTransferred := 0; repeat lRes := GetOverlappedResult( lFileHandle, lOverlapped, lTransferred, False ); if not lRes then lErr2 := GetLastError; if not lRes then case lErr2 of //проверка ошибки GetOverlappedResult ERROR_IO_INCOMPLETE: Application.HandleMessage; //операция не завершилась? обработаем очередное сообщение ERROR_HANDLE_EOF: begin //файл кончился? lReaded := 0; //ничего не прочитали break; //выходим из внутреннего цикла end; else RaiseLastOSError; end; until lRes; //пока не завершиться операция… if lRes then lReaded := lTransferred; //надо ли? end; ERROR_HANDLE_EOF: lReaded := 0; //если файл кончилсо, то выходим из цикла else RaiseLastOSError; end; end;//конец ветки, когда ReadFile = false if lRes then //здесь мы оказываемся в 2х случаях:1.ReadFile=true; 2.GetOverlappedResult в конце концов тоже вернул True begin // //что-то делаем с lBuff // lOverlapped.Offset := lOverlapped.Offset + lReaded; //сдвигаем end; until lReaded = 0; finally FreeMem( lBuff ); end; finally CloseHandle( lFileHandle ); end; end;
все бы ничего… да только не всегда получается асинхронный вызов. Сообщение отредактировано: MetalFan — 19.12.07, 09:34 |
Ahilles |
|
Full Member Рейтинг (т): 10 |
неплохая статья про асинхронный ввод-вывод. |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Ahilles Спасибо конечно, но ничего нового статья мне не рассказала. вся информация есть в MSDN. |
medved_68 |
|
Цитата MetalFan @ 16.12.07, 10:21 все бы ничего… да только не всегда получается асинхронный вызов.
Вернее — работает не пойми как. Цитата MetalFan @ 16.12.07, 10:21 //hEvent не задаю, так как нигде ожидать ничего не будем lRes := ReadFile( lFileHandle, lBuff^, C_BLOCK_SIZE, lReaded, @lOverlapped); И?? Как же так — пытаешься использовать асинхронное чтение, а главный «семафор окончания отложенной операции» не создаешь? Далее : Цитата MetalFan @ 16.12.07, 10:21 if not lRes then case lErr of //проверка ощибки ReadFile ERROR_IO_PENDING: //асинхронная операция Это не ошибка — это признак того, что отложенная операция нормально стартовала. Здесь бы лучше запустить таймер и по его срабатыванию проверять — закончилась ли операция чтения (считано ли требуемое в ReadFile количество байт (WaitForSingleObject на эвент Overlapp’a должен вернуть WAIT_OBJECT_0 — если нет — то по GetOverlappedResult смотрим, сколько считано и сравниваем с размером файла, или по флагу ERROR_HANDLE_EOF принимаем решение сбросить дальнейшее чтение). Но лучше в любом случае перед считыванием получить размер файла, чтобы знать наверняка, сколько читать.) |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Цитата medved_68 @ 17.12.07, 05:38 И?? Как же так — пытаешься использовать асинхронное чтение, а главный «семафор окончания отложенной операции» не создаешь? создавать и использовать его не обязательно. но даже его создание и использование положения не меняет. Цитата medved_68 @ 17.12.07, 05:38 Это не ошибка — это признак того, что отложенная операция нормально стартовала.
отложенная операция удачно «стартовала» только если GetLastError вернул ERROR_IO_PENDING. Цитата medved_68 @ 17.12.07, 05:38 сколько считано и сравниваем с размером файла как ты заметил, файл читается не целиком, а кускакми з.ы. ты бы проверил работу кода, ибо пока ничего неясного ты не прояснил. Сообщение отредактировано: MetalFan — 17.12.07, 06:31 |
—Ins— |
|
M MetalFan, я ваш вопрос отредактировал, код не подсвечивался. Там в теге CODE было CODE=delphi, а нужно — CODE=pas, ну это так, на будущее. Что касается вопроса — думаю, не нужно вызывать Application.HandleMessage; так как эта процедура в конце вызывает засыпание потока на неопределенное время — пока в очередь не поступит сообщение (Idle из которого вызывается WaitMessage). Как обрабатывать сообщения и одновременно дожидаться окончания ввода/вывода гляньте здесь: Там вся соль в функции MsgWaitForMultipleObjects |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Rose Сорри, с винграда скопировал и не заметил ньюанса с pas. Добавлено 19.12.07, 09:00 Цитата Rose @ 19.12.07, 08:53 так как эта процедура в конце вызывает засыпание потока
возможно… но не в этом проблема… |
—Ins— |
|
Цитата возможно… но не в этом проблема… Все же попробуй переделать свой код на менер того, как в приведенной ссылке — Event заведи, MsgWaitForMultipleObjects используй, ProcessMessages вместо HandleMessage, все должно работать. |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Rose Да нет! ты не понял… ветка кода с GetOverlappedResult и так работает на «ура»! но просто при чтении с ЖД FileRead всегда возвращает true. |
—Ins— |
|
Цитата Rose Да нет! ты не понял… Видимо действительно не понял. Вторая попытка |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Rose можно на «ты»? ) а то «ты мне тут не выкайте») Цитата Rose @ 19.12.07, 09:27 ReadFile не обязана вернуть код ошибки ERROR_IO_PENDING
именно! в коде обрабатываются оба варианта операции… Цитата Rose @ 19.12.07, 09:27 Может у Вас именно этот случай? Или на ReadFile код надолго останавливается?
у меня именно этот случай при работе с HDD!!!! Сообщение отредактировано: MetalFan — 19.12.07, 09:38 |
—Ins— |
|
Цитата Rose можно на «ты»? ) а то «ты мне тут не выкайте»)
Привычка от других форумов, где принято на «Вы» Попробую твой код в ближайшее время, пока сам не увижу — наверное не пойму в чем проблема. |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Rose угу, пасип. Цитата MetalFan @ 19.12.07, 09:37 как я уже ранее предположил, система скорее всего с жесткого считывает быстро, а вот мой 50Мб-йтный буфер заполняется как раз эти 0,5-1 секунды задержки… |
—Ins— |
|
Цитата как я уже ранее предположил, система скорее всего с жесткого считывает быстро, а вот мой 50Мб-йтный буфер заполняется как раз эти 0,5-1 секунды задержки… В общем, мои исследования привели меня к выводу, что все дело в кешировании файлов. Если флаг FILE_FLAG_NO_BUFFERING не установлен, то первое обращение к файлу может занять несколько большее время, а последующие происходят значительно быстрее. Если установлен — то механиз кеширования не применяется, однако при этом нужно соблюсти некоторые требования, о которых в справке написано. Короче, то, что у тебя происходит — это нормальное поведение. |
MetalFan |
|
Full Member Рейтинг (т): 14 |
Цитата Rose @ 19.12.07, 11:07 Если флаг FILE_FLAG_NO_BUFFERING не установлен
проверял и с ним и без него. Добавлено 19.12.07, 11:16 |
0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
0 пользователей:
- Предыдущая тема
- Delphi: Система, Windows API
- Следующая тема
[ Script execution time: 0,0783 ] [ 16 queries used ] [ Generated: 9.02.23, 09:51 GMT ]
Delphi: работа с COM-портом
Оригинальное название статьи — «Вид на порт из окна» — прим. ред.
Вопрос «как работать с COM-портами?» стал классическим на многих конференциях по языкам программирования. Рано или поздно чуть не каждому программисту приходится работать с портами ввода/вывода. Сегодня я хочу рассказать про работу с последовательным портом из-под самой распространенной на сегодняшний день 32-разрядной операционной системы — Windows. К статье прилагается пример программы, работающей с COM-портом, написанной на Borland Delphi 7.
Статья построена по принципу «от простого к сложному». Сначала будут изложены основы работы с портами из-под Win32 с описанием необходимых функций. Затем рассмотрим применение этих функций на примере Delphi-программы. Конечным результатом будет класс, предназначенный для работы с COM-портом, и пример использующей его программы.
Очень часто программисту приходится управлять с помощью компьютера каким-либо внешним устройством, или просто анализировать состояние этого устройства. Порты ввода/вывода — самый распространенный способ сопряжения компьютера и внешнего устройства. Давным-давно уже написано множество классов, библиотек и компонент для работы с портами, поэтому можно, конечно, воспользоваться уже готовым и к тому же бесплатным решением. Именно так я и поступил лет семь назад, при этом потеряв самое главное — своевременное понимание того, как все-таки работать с портами из-под Win32. Незнание внутренних механизмов — это, во-первых, пробел в стройном ряду знаний, а во-вторых, актуальная возможность ошибок в работе программы.
С портами из-под Win32 работают так же, как и с обычными файлами, используя при этом всего несколько специфичных функций WinAPI. Однако коммуникационный порт — это не совсем обычный файл. Для него, например, нельзя выполнить позиционирование файлового указателя, или же создать порт, если таковой отсутствует. Любая работа с портом начинается с его открытия. Для этого используется файловая функция WinAPI (описания WinAPI-функций взяты из MSDN (Microsoft Developer Network), следовательно, приводятся в синтаксисе C):
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile );
lpFileName — указатель на строку с нулевым завершающим символом. Обычно это имя открываемого файла, но в нашем случае это должно быть название порта (COM1, COM2, …).
dwDesiredAccess — тип доступа. В нашем случае должен быть равен GENERIC_READ|GENERIC_WRITE.
dwShareMode — параметр совместного доступа. Для коммуникационных портов всегда равен 0.
lpSecurityAttributes — атрибут защиты. Для коммуникационных портов всегда равен NULL.
dwCreationDistribution — режим автосоздания. Для коммуникационных портов всегда равен OPEN_EXESTING.
dwFlagsAndAttributes — атрибут режима обработки. Для коммуникационных портов должен быть равен 0 или FILE_FLAG_OVERLAPPED.
hTemplateFile — описатель файла-шаблона. Для коммуникационных портов должен быть равен NULL.
При успешном открытии порта функция возвращает его описатель, а в случае ошибки возвращает INVALID_HANDLE_VALUE.
Сразу оговорюсь: все недостающие описания можно найти на http://msdn.microsoft.comи еще по ряду адресов, которые вам подскажет поисковый сервер.
Из всех параметров функции CreateFile() особого пояснения требует dwFlagsAndAttributes. Работа с портом может быть организована в синхронном (nonoverlapped) или асинхронном (overlapped) режимах обработки, что и задается этим флагом. При синхронном режиме (когда параметр dwFlagsAndAttributes = 0) только один поток приложения может либо читать, либо писать в порт. Помните переговорное устройство в лифте? Нажали кнопку — можем только говорить, отпустили кнопку — можем только слушать.
Синхронный режим обработки прост в реализации. Если надо записать данные в порт, то вызываем функцию записи и ожидаем, пока она не завершится. Если же надо читать данные, то вызываем функцию чтения и ждем, пока она не отработает. Для простых задач синхронный режим обработки вполне подходит, однако в мире Windows он почти всегда обречен на неудачу. Ожидание операции чтения или записи воспринимается пользователем программы как «зависание».
Асинхронный режим (когда параметр dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED) позволяет производить операции чтения и записи в порт параллельно из разных потоков. В то время, пока один поток приложения принимает данные, другой поток может параллельно с первым передавать данные — как при разговоре по телефону, когда вы можете слушать и говорить одновременно. Данный режим обработки больше импонирует идее многозадачности Windows. Но за все надо платить: для реализации этого режима обработки требуется в два раза больше написанного кода, вдобавок, умения писать многопоточные программы. Какой режим выбрать — решайте сами. Но если уж разбираться в работе порта, то разбираться «по-взрослому», до конца, а потому и рассмотрим более сложный вариант — асинхронную обработку.
На практике открытие порта для асинхронного режима обработки из программы на Delphi выглядит примерно так:
hPort := CreateFile(‘COM1’, GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0); if hPort = INVALID_HANDLE_VALUE then raise Exception.Create(‘Error opening port’);
Функция возвращает описатель порта (hPort), который нам потом пригодится для вызова других функций работы с портом. Если в результате открытия порта описатель не получен, то возбуждается исключение с соответствующим текстом ошибки. Открыв порт, мы получаем его в свое распоряжение. Теперь с этим портом может работать только наша программа (точнее, только наш процесс). По окончании работы с портом его следует закрыть, вызвав функцию:
BOOL CloseHandle( HANDLE hObject );
В качестве единственного параметра надо передать полученный ранее описатель порта (hPort).
Хоть система при завершении выполнения программы и освобождает все выделенные ей ресурсы (в том числе и порты), хорошим тоном программирования считается собственноручное закрытие портов. Открывать/закрывать порт как будто несложно. Кроме того, нам потребуется программная настройка порта. Думаю, все видели диалог настройки последовательного порта в диспетчере устройств системы. Все эти настройки мы можем произвести программно. Для этих целей используется функция WinAPI:
BOOL SetCommState( HANDLE hFile, LPDCB lpDCB );
hFile — описатель открытого порта.
lpDCB — указатель на структуру DCB.
Основные параметры последовательного порта описываются структурой DCB. Она содержит массу полей, каждое из которых соответствует определенному параметру настройки порта. Мы рассмотрим несколько полей, которые нам нужны:
BaudRate — скорость передачи данных. Возможно указание констант —CBR_100, CBR_300, CBR_600, CBR_1200, …, CBR_256000.
Parity — схема контроля четности. Может содержать одно из следующих значений: EVENPARITY, MARKPARITY, NOPARITY, ODDPARITY, SPACEPARITY.
ByteSize — число информационных бит в передаваемых и принимаемых байтах.
StopBits — количество стоповых бит. Может быть ONESTOPBIT, ONE5STOPBIT, TWOSTOPBIT.
Чтобы не заполнять структуру DCB вручную, ее можно заполнить информацией о текущем состоянии порта вызовом функции GetCommState(), затем изменить необходимые поля и установить настройки вызовом функции SetCommState(). Настройку порта желательно производить сразу после его открытия. На Delphi это выглядит так:
var Dcb: TDcb; … if not GetCommState(hPort, Dcb) then raise Exception.Create(‘Error setting port state’); Dcb.BaudRate := CBR_9600; Dcb.Parity := NOPARITY; Dcb.ByteSize := 8; Dcb.StopBits := ONESTOPBIT; if not SetCommState(hPort, Dcb) then raise Exception.Create(‘Error setting port state’);
Еще одна операция, которая нам понадобится сразу после открытия порта — его сброс.
BOOL PurgeComm( HANDLE hFile, DWORD dwFlags );
Вызов этой функции очищает очередь приема/передачи и завершает все находящиеся в ожидании запросы ввода/вывода.
hFile — описатель открытого порта.
dwFlags — производимые действия в виде набора флагов PURGE_TXABORT, PURGE_RXABORT, PURGE_TXCLEAR, PURGE_RXCLEAR.
Пример на Delphi:
if not PurgeComm(hPort, PURGE_TXCLEAR or PURGE_RXCLEAR) then raise Exception.Create(‘Error purging port’);
На этом подготовительная фаза заканчивается, и можно приступать непосредственно к приему/передаче данных. Прием данных у нас будет происходить по событийной схеме; программа будет ожидать прием одного или нескольких символов (байт). Для перевода порта в этот режим необходимо вызвать функцию SetCommMask() с флагом EV_RXCHAR:
if not SetCommMask(hPort, EV_RXCHAR) then raise Exception.Create(‘Error setting port mask’);
Прием и передача данных выполняется функциями ReadFile() и WriteFile(), то есть теми же самыми функциями, которые используются для работы с дисковыми файлами. Вот их описание:
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped );
hFile — описатель открытого порта.
lpBuffer — адрес буфера.
nNumberOfBytesToRead/nNumberOfBytesToWrite — число ожидаемых к приему или предназначенных для передачи байт.
lpNumberOfBytesRead/lpNumberOfBytesWritten — число фактически принятых или переданных байт.
lpOverlapped — адрес структуры OVERLAPPED, используемой для асинхронных операций.
Передача данных является довольно быстрой операцией, поэтому как правило ее выполняют из главного потока приложения. На Delphi это выглядит так:
var dwWrite: DWORD; OverWrite: TOverlapped; WriteBytes: array of Byte; … begin OverWrite.hEvent := CreateEvent(nil, True, False, nil); if OverWrite.hEvent = Null then raise Exception.Create(‘Error creating write event’); … if (not WriteFile(hPort, WriteBytes, SizeOf(WriteBytes), dwWrite, @OverWrite)) and (GetLastError <> ERROR_IO_PENDING) then raise Exception.Create(‘Error writing port’); end;
В данном примере функция WriteFile() выполняет асинхронную запись массива байтов WriteBytes в порт. Она сразу возвращает управление, и запись в порт происходит параллельно с выполнением основного кода потока. Если результат WriteFile() равен False, то это значит, что на момент возврата управления передача массива байтов еще не закончилась. Поэтому код ошибки выполнения WriteFile() в данном случае должен быть равен ERROR_IO_PENDING. Переменная OverWrite — overlapped-структура, необходимая для асинхронных операций.
В принципе, вас не должно волновать, когда закончится передача массива байтов. Зато момент приема одного или нескольких символов действительно важен. Поэтому его можно разбить на две части: инициирование приема и определение момента приема с последующим чтением символов. Поскольку при этом приходится считаться с фактором ожидания приема символа, рекомендуется функции приема данных вынести в отдельный поток. Передавать данные можно и из основного потока приложения, поскольку это происходит довольно быстро. А вот событие приема символа будем ожидать в отдельном потоке.
Рассмотрение работы с потоками в Windows, в частности того, как это реализовано в Delphi, выходит за рамки данной статьи. Предполагаю, что читатель встречался или по крайней мере знаком с этим. Скажу лишь, что у любого потока есть главная функция, которая начинает выполняться после его создания. В Delphi для потоков существует класс TThread, а его главная процедура называется TThread.Execute().
Вот так выглядит главная процедура отдельного потока, которая ожидает появление одного или нескольких символов и считывает их:
procedure TReadThread.Execute; var ComStat: TComStat; dwMask, dwError: DWORD; OverRead: TOverlapped; Buf: array[0..$FF] of Byte; dwRead: DWORD; begin OverRead.hEvent := CreateEvent(nil, True, False, nil); if OverRead.hEvent = Null then raise Exception.Create(‘Error creating read event’); FreeOnTerminate := True; while not Terminated do begin if not WaitCommEvent(hPort, dwMask, @OverRead) then begin if GetLastError = ERROR_IO_PENDING then WaitForSingleObject(OverRead.hEvent, INFINITE) else raise Exception.Create(‘Error waiting port event’); end; if not ClearCommError(hPort, dwError, @ComStat) then raise Exception.Create(‘Error clearing port’); dwRead := ComStat.cbInQue; if dwRead > 0 then begin if not ReadFile(hPort, Buf, dwRead, dwRead, @OverRead) then raise Exception.Create(‘Error reading port’); // В Buf находятся прочитанные байты // Далее идет обработка принятых байтов end; end; {while} end;
В приведенном примере в потоке крутится цикл, тем самым инициируется ожидание события порта вызовом функции WaitCommEvent(), ожидание же самого этого события задается функцией WaitForSingleObject(). Для определения количества принятых символов используется функция ClearCommError(). Когда количество принятых символов (dwRead) известно, непосредственное чтение символов выполняется функцией ReadFile().
Используя вышеописанные выкладки, я написал на Borland Delphi 7 класс TComPort для работы с COM-портами. К классу прилагается пример приложения, использующий его. Для проверки работоспособности программы я просто соединил нуль-модемным кабелем два COM-порта на своем компьютере и запустил два экземпляра программы для каждого порта. Данные передаются через один порт и одновременно принимаются через другой. Скриншот программы прилагается.
Для передачи и приема данных предусмотрены отдельные окна. Формат передаваемых данных — строка. Принимаемые данные представляются в виде массива байт.
Работа с COM-портами в Delphi.
Вопрос «как работать с COM-портами?» стал классическим на многих конференциях
по языкам программирования. Рано или поздно чуть не каждому программисту
приходится работать с портами ввода/вывода. Сегодня я хочу рассказать
про работу с последовательным портом из-под самой распространенной на
сегодняшний день 32-разрядной операционной системы — Windows. Статья построена
по принципу «от простого к сложному». Сначала будут изложены основы работы
с портами из-под Win32 с описанием необходимых функций. Затем рассмотрим
применение этих функций на примере Delphi-программы. Конечным результатом
будет класс, предназначенный для работы с COM-портом, и пример использующей
его программы.
По ссылке Serial Net Tools — находится написанная мной программа для работы с COM-портом.
Очень часто программисту приходится управлять с помощью компьютера
каким-либо внешним устройством, или просто анализировать состояние этого
устройства. Порты ввода/вывода — самый распространенный способ сопряжения
компьютера и внешнего устройства. Давным-давно уже написано множество
классов, библиотек и компонент для работы с портами, поэтому можно,
конечно, воспользоваться уже готовым и к тому же бесплатным решением.
Именно так я и поступил лет семь назад, при этом потеряв самое главное
— своевременное понимание того, как все-таки работать с портами из-под
Win32. Незнание внутренних механизмов — это, во-первых, пробел в стройном
ряду знаний, а во-вторых, актуальная возможность ошибок в работе программы.
С портами из-под Win32 работают так же, как и с обычными файлами, используя
при этом всего несколько специфичных функций WinAPI. Однако коммуникационный
порт — это не совсем обычный файл. Для него, например, нельзя выполнить
позиционирование файлового указателя, или же создать порт, если таковой
отсутствует. Любая работа с портом начинается с его открытия. Для этого
используется файловая функция WinAPI (описания WinAPI-функций взяты
из MSDN (Microsoft Developer Network), следовательно, приводятся в синтаксисе
C):
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);lpFileName — указатель на строку с нулевым завершающим символом. Обычно
это имя открываемого файла, но в нашем случае это должно быть название
порта (COM1, COM2, …).
dwDesiredAccess — тип доступа. В нашем случае должен быть равен GENERIC_READ|GENERIC_WRITE.
dwShareMode — параметр совместного доступа. Для коммуникационных портов всегда равен 0.
lpSecurityAttributes — атрибут защиты. Для коммуникационных портов всегда равен NULL.
dwCreationDistribution — режим автосоздания. Для коммуникационных портов
всегда равен OPEN_EXESTING.
dwFlagsAndAttributes — атрибут режима обработки. Для коммуникационных
портов должен быть равен 0 или FILE_FLAG_OVERLAPPED.
hTemplateFile — описатель файла-шаблона. Для коммуникационных портов
должен быть равен NULL.
При успешном открытии порта функция возвращает его описатель, а в случае
ошибки возвращает INVALID_HANDLE_VALUE.
Сразу оговорюсь: все недостающие описания можно найти на http://msdn.microsoft.comи
еще по ряду адресов, которые вам подскажет поисковый сервер.
Из всех параметров функции CreateFile() особого пояснения требует dwFlagsAndAttributes.
Работа с портом может быть организована в синхронном (nonoverlapped)
или асинхронном (overlapped) режимах обработки, что и задается этим
флагом. При синхронном режиме (когда параметр dwFlagsAndAttributes =
0) только один поток приложения может либо читать, либо писать в порт.
Помните переговорное устройство в лифте? Нажали кнопку — можем только
говорить, отпустили кнопку — можем только слушать.
Синхронный режим обработки прост в реализации. Если надо записать данные
в порт, то вызываем функцию записи и ожидаем, пока она не завершится.
Если же надо читать данные, то вызываем функцию чтения и ждем, пока
она не отработает. Для простых задач синхронный режим обработки вполне
подходит, однако в мире Windows он почти всегда обречен на неудачу.
Ожидание операции чтения или записи воспринимается пользователем программы
как «зависание».
Асинхронный режим (когда параметр dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED)
позволяет производить операции чтения и записи в порт параллельно из
разных потоков. В то время, пока один поток приложения принимает данные,
другой поток может параллельно с первым передавать данные — как при
разговоре по телефону, когда вы можете слушать и говорить одновременно.
Данный режим обработки больше импонирует идее многозадачности Windows.
Но за все надо платить: для реализации этого режима обработки требуется
в два раза больше написанного кода, вдобавок, умения писать многопоточные
программы. Какой режим выбрать — решайте сами. Но если уж разбираться
в работе порта, то разбираться «по-взрослому», до конца, а потому и
рассмотрим более сложный вариант — асинхронную обработку.
На практике открытие порта для асинхронного режима обработки из программы
на Delphi выглядит примерно так:
hPort := CreateFile(‘COM1’, GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
if hPort = INVALID_HANDLE_VALUE then
raise Exception.Create(‘Error opening port’);Функция возвращает описатель
порта (hPort), который нам потом пригодится для вызова других функций
работы с портом. Если в результате открытия порта описатель не получен,
то возбуждается исключение с соответствующим текстом ошибки. Открыв
порт, мы получаем его в свое распоряжение. Теперь с этим портом может
работать только наша программа (точнее, только наш процесс). По окончании
работы с портом его следует закрыть, вызвав функцию:
BOOL CloseHandle(
HANDLE hObject
);В качестве единственного параметра надо передать полученный ранее
описатель порта (hPort).
Хоть система при завершении выполнения программы и освобождает все
выделенные ей ресурсы (в том числе и порты), хорошим тоном программирования
считается собственноручное закрытие портов. Открывать/закрывать порт
как будто несложно. Кроме того, нам потребуется программная настройка
порта. Думаю, все видели диалог настройки последовательного порта в
диспетчере устройств системы. Все эти настройки мы можем произвести
программно. Для этих целей используется функция WinAPI:
BOOL SetCommState(
HANDLE hFile,
LPDCB lpDCB
);hFile — описатель открытого порта.
lpDCB — указатель на структуру DCB.
Основные параметры последовательного порта описываются структурой DCB.
Она содержит массу полей, каждое из которых соответствует определенному
параметру настройки порта. Мы рассмотрим несколько полей, которые нам
нужны:
BaudRate — скорость передачи данных. Возможно указание констант —CBR_100,
CBR_300, CBR_600, CBR_1200, …, CBR_256000.
Parity — схема контроля четности. Может содержать одно из следующих
значений: EVENPARITY, MARKPARITY, NOPARITY, ODDPARITY, SPACEPARITY.
ByteSize — число информационных бит в передаваемых и принимаемых байтах.
StopBits — количество стоповых бит. Может быть ONESTOPBIT, ONE5STOPBIT,
TWOSTOPBIT.
Чтобы не заполнять структуру DCB вручную, ее можно заполнить информацией
о текущем состоянии порта вызовом функции GetCommState(), затем изменить
необходимые поля и установить настройки вызовом функции SetCommState().
Настройку порта желательно производить сразу после его открытия. На
Delphi это выглядит так:
var
Dcb: TDcb;
…
if not GetCommState(hPort, Dcb) then
raise Exception.Create(‘Error setting port state’);
Dcb.BaudRate := CBR_9600;
Dcb.Parity := NOPARITY;
Dcb.ByteSize := 8;
Dcb.StopBits := ONESTOPBIT;
if not SetCommState(hPort, Dcb) then
raise Exception.Create(‘Error setting port state’);
Еще одна операция, которая нам понадобится сразу после открытия порта
— его сброс.
BOOL PurgeComm(
HANDLE hFile,
DWORD dwFlags
);Вызов этой функции очищает очередь приема/передачи и завершает все
находящиеся в ожидании запросы ввода/вывода.
hFile — описатель открытого порта.
dwFlags — производимые действия в виде набора флагов PURGE_TXABORT,
PURGE_RXABORT, PURGE_TXCLEAR, PURGE_RXCLEAR.
Пример на Delphi:
if not PurgeComm(hPort, PURGE_TXCLEAR or PURGE_RXCLEAR) then
raise Exception.Create(‘Error purging port’);На этом подготовительная
фаза заканчивается, и можно приступать непосредственно к приему/передаче
данных. Прием данных у нас будет происходить по событийной схеме; программа
будет ожидать прием одного или нескольких символов (байт). Для перевода
порта в этот режим необходимо вызвать функцию SetCommMask() с флагом
EV_RXCHAR:
if not SetCommMask(hPort, EV_RXCHAR) then
raise Exception.Create(‘Error setting port mask’);Прием и передача данных
выполняется функциями ReadFile() и WriteFile(), то есть теми же самыми
функциями, которые используются для работы с дисковыми файлами. Вот
их описание:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
hFile — описатель открытого порта.
lpBuffer — адрес буфера.
nNumberOfBytesToRead/nNumberOfBytesToWrite — число ожидаемых к приему
или предназначенных для передачи байт.
lpNumberOfBytesRead/lpNumberOfBytesWritten — число фактически принятых
или переданных байт.
lpOverlapped — адрес структуры OVERLAPPED, используемой для асинхронных
операций.
Передача данных является довольно быстрой операцией, поэтому как правило
ее выполняют из главного потока приложения. На Delphi это выглядит так:
var
dwWrite: DWORD;
OverWrite: TOverlapped;
WriteBytes: array of Byte;
…
begin
OverWrite.hEvent := CreateEvent(nil, True, False, nil);
if OverWrite.hEvent = Null then
raise Exception.Create(‘Error creating write event’);
…
if (not WriteFile(hPort, WriteBytes, SizeOf(WriteBytes),
dwWrite, @OverWrite))
and (GetLastError <> ERROR_IO_PENDING) then
raise Exception.Create(‘Error writing port’);
end;В данном примере функция WriteFile() выполняет асинхронную запись
массива байтов WriteBytes в порт. Она сразу возвращает управление, и
запись в порт происходит параллельно с выполнением основного кода потока.
Если результат WriteFile() равен False, то это значит, что на момент
возврата управления передача массива байтов еще не закончилась. Поэтому
код ошибки выполнения WriteFile() в данном случае должен быть равен
ERROR_IO_PENDING. Переменная OverWrite — overlapped-структура, необходимая
для асинхронных операций.
В принципе, вас не должно волновать, когда закончится передача массива
байтов. Зато момент приема одного или нескольких символов действительно
важен. Поэтому его можно разбить на две части: инициирование приема
и определение момента приема с последующим чтением символов. Поскольку
при этом приходится считаться с фактором ожидания приема символа, рекомендуется
функции приема данных вынести в отдельный поток. Передавать данные можно
и из основного потока приложения, поскольку это происходит довольно
быстро. А вот событие приема символа будем ожидать в отдельном потоке.
Рассмотрение работы с потоками в Windows, в частности того, как это
реализовано в Delphi, выходит за рамки данной статьи. Предполагаю, что
читатель встречался или по крайней мере знаком с этим. Скажу лишь, что
у любого потока есть главная функция, которая начинает выполняться после
его создания. В Delphi для потоков существует класс TThread, а его главная
процедура называется TThread.Execute().
Вот так выглядит главная процедура отдельного потока, которая ожидает
появление одного или нескольких символов и считывает их:
procedure TReadThread.Execute;
var
ComStat: TComStat;
dwMask, dwError: DWORD;
OverRead: TOverlapped;
Buf: array[0..$FF] of Byte;
dwRead: DWORD;
begin
OverRead.hEvent := CreateEvent(nil, True, False, nil);
if OverRead.hEvent = Null then
raise Exception.Create(‘Error creating read event’);
FreeOnTerminate := True;
while not Terminated do
begin
if not WaitCommEvent(hPort, dwMask, @OverRead) then
begin
if GetLastError = ERROR_IO_PENDING then
WaitForSingleObject(OverRead.hEvent, INFINITE)
else
raise Exception.Create(‘Error waiting port event’);
end;
if not ClearCommError(hPort, dwError, @ComStat) then
raise Exception.Create(‘Error clearing port’);
dwRead := ComStat.cbInQue;
if dwRead > 0 then
begin
if not ReadFile(hPort, Buf, dwRead, dwRead, @OverRead) then
raise Exception.Create(‘Error reading port’);
// В Buf находятся прочитанные байты
// Далее идет обработка принятых байтов
end;
end; {while}
end;
В приведенном примере в потоке крутится цикл, тем самым инициируется
ожидание события порта вызовом функции WaitCommEvent(), ожидание же
самого этого события задается функцией WaitForSingleObject(). Для определения
количества принятых символов используется функция ClearCommError().
Когда количество принятых символов (dwRead) известно, непосредственное
чтение символов выполняется функцией ReadFile().
Используя вышеописанные выкладки, я написал на Borland Delphi 7 класс
TComPort для работы с COM-портами. Для проверки работоспособности программы
я просто соединил нуль-модемным кабелем два COM-порта на своем компьютере
и запустил два экземпляра программы для каждого порта. Данные передаются
через один порт и одновременно принимаются через другой.
Для передачи и приема данных предусмотрены отдельные окна. Формат передаваемых
данных — строка. Принимаемые данные представляются в виде массива байт.
Статьи на смежную тематику:
Allen Denver, Serial Communications in Win32 ( http://msdn.microsoft.com/library/en-us/dnfiles/html/msdn_serial.asp).
Дмитрий Кузан, Работа с портами ввода-вывода в Delphi ( http://www.delphikingdom.ru/mastering/ports1.htm).
Олег Титов, Работа с коммуникационными портами (COM и LPT) в программах для Win32 ( http://www.happytown.ru/prog/practika/com_win32.html).
Оригинал статьи http://www.mycomp.com.ua/. Автор статьи Игорь ПАВЛОВ.