Взаимодействие между процессами – ключевая часть многих современных приложений. Один из способов реализовать это взаимодействие – использование именованных каналов. В Windows именованные каналы представляют собой специализированный механизм IPC (Inter-Process Communication), который могут быть использованы для асинхронного или синхронного обмена данными между сервером и клиентами. В POSIX-совместимых системах аналогичная функциональность достигается через такие механизмы, как FIFO (first-in, first-out) файлы, которые предоставляют однонаправленный канал для передачи данных.
Особенности в Windows
- Двунаправленность: Один именованный канал в Windows может использоваться как для чтения, так и для записи данных.
- Настройки безопасности: Можно настроить разрешения для доступа к каналу, контролируя, кто может читать, писать или создавать новые экземпляры канала.
- Типы сообщений: Поддержка как потоковых, так и сообщений с ориентированными передачами данных.
Особенности в POSIX
- Однонаправленность: Каждый FIFO файл предназначен либо для чтения, либо для записи относительно одной стороны (например, сервера).
- Простота: FIFO файлы представляют собой специальные файлы в файловой системе и используются аналогично обычным файлам.
Пример реализации на D
Пример сервера
import std.stdio;
import std.process;
import std.string;
import std.typecons : RefCounted;
import std.uuid;
void main()
{
string pipeName = randomUUID().toString;
auto serverPipe = NamedPipe(pipeName);
writeln("Server: Create a named pipe.");
auto childProcess = spawnProcess(["./client", pipeName]);
//auto childProcess = spawnProcess(["java", "-jar", "client.jar", pipeName]); // launching a client in java
writeln("Server: Launch the client.");
auto pipeFile = serverPipe.connect();
writeln("Server: Connection established.");
// Sending a message to a client
pipeFile.writeln("hello children");
pipeFile.flush();
writeln("Server: Message sent.");
// Reading a response from a client
string response = pipeFile.readln();
writeln("Server: Response received -", response.strip);
childProcess.wait();
writeln("Server: Shutting down.");
}
struct NamedPipeImpl
{
immutable string fileName;
/// Create a named pipe, and reserve a filename.
this()(string name)
{
version (Windows)
{
import core.sys.windows.windows;
SECURITY_ATTRIBUTES sa;
sa.nLength = SECURITY_ATTRIBUTES.sizeof;
sa.bInheritHandle = TRUE; // Allow inheritance of handle
sa.lpSecurityDescriptor = null; // Use the parent process's security descriptor
fileName = `\\.\pipe\` ~ name;
auto h = CreateNamedPipe(fileName.toUTF16z(), PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1, 4096, 4096, NMPWAIT_USE_DEFAULT_WAIT, &sa)
.wenforce("CreateNamedPipe");
f.windowsHandleOpen(h, "wb+");
}
else
{
import core.sys.posix.sys.stat;
fileName = `/tmp/` ~ name ~ `.fifo`;
mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR);
}
}
/// Wait for a peer to open the other end of the pipe.
File connect()()
{
version (Windows)
{
import core.sys.windows.windows;
import core.sys.windows.windef;
BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null);
// "If a client connects before the function is called, the function returns zero
// and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client
// connects in the interval between the call to CreateNamedPipe and the call to
// ConnectNamedPipe. In this situation, there is a good connection between client
// and server, even though the function returns zero."
if (!bSuccess)
wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe");
return f;
}
else
{
return File(fileName, "w");
}
}
~this()
{
version (Windows)
{
// File.~this will take care of cleanup
}
else
fileName.remove();
}
private:
File f;
}
alias NamedPipe = RefCounted!NamedPipeImpl;
version (Windows):
import core.sys.windows.windows;
import std.string;
class WindowsException : Exception
{
DWORD code; /// The error code.
this(DWORD code, string str=null)
{
this.code = code;
wchar *lpMsgBuf = null;
FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
null,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
cast(LPWSTR)&lpMsgBuf,
0,
null);
auto message = lpMsgBuf.fromWString();
if (lpMsgBuf)
LocalFree(lpMsgBuf);
message = strip(message);
message ~= format(" (error %d)", code);
if (str)
message = str ~ ": " ~ message;
super(message);
}
}
import std.utf;
/// Convert from a potentially zero-terminated wide string.
string fromWString(in wchar[] buf)
{
foreach (i, c; buf)
if (!c)
return toUTF8(buf[0 .. i]);
return toUTF8(buf);
}
/// Convert from a zero-terminated wide string.
string fromWString(in wchar* buf)
{
if (!buf)
return null;
const(wchar)* p = buf;
for (; *p; p++)
{
}
return toUTF8(buf[0 .. p - buf]);
}
/// Convert to a zero-terminated wide string.
LPCWSTR toWStringz(string s)
{
return s is null ? null : toUTF16z(s);
}
T wenforce(T)(T cond, string str=null)
{
if (cond)
return cond;
throw new WindowsException(GetLastError(), str);
}В этом коде мы изолировали работу с именованным каналом в структуру NamedPipeImpl, которая занимается всей работой по созданию, настройке и открытию канала в Windows или POSIX системах. Также для Windows добавлена удобная обработка ошибок. За NamedPipeImpl и обработку ошибок спасибо создателям библиотеки AE во главе с Владимиром Пантелеевым. NamedPipe позволяет автоматически освобождать ресурсы при завершении работы с каналом. В SECURITY_ATTRIBUTES настроено наследование дескриптора процесса дочерним процессом, что позволяет запретить подключение к каналу не дочерних процессов, а также в аргументах функции CreateNamedPipe выставлена возможность создания только одного экземпляра канала, то есть подключиться сможет только 1 клиент. В интернете вы можете найти подробное описание и документацию всех используемых функций и аргументов Windows и POSIX API.
Пример клиента
import std.logger;
import std.stdio;
void main(string[] args)
{
Logger logger = new FileLogger("out.log");
string pipePath = r"\\.\pipe\";
if (args.length < 2)
{
logger.info("Usage: client <channel name>");
return;
}
string pipeName = pipePath ~ args[1];
auto pipeFile = File(pipeName, "r+"); // Use "r+" to read and write
logger.info("Client: Connection established.");
// Reading a message from the server
string message = pipeFile.readln();
logger.info("Client: Message received - ", message);
// Sending a response to the server
pipeFile.writeln("hello parent");
pipeFile.flush();
logger.info("Client: Response sent.");
logger.info("Client: Shut down.");
}В этом коде клиент работает с каналом как с обычным файлом по определенному пути. Обратите внимание, что для POSIX путь нужно изменить в соответствии с путем, определенным в NamedPipeImpl. Кроме того, в POSIX для двунаправленной связи придется создавать 2 канала, как мы уже писали выше. Один канал на сервере должен быть настроен на запись, другой на прием, соответственно, на клиенте все наоборот. В примере кода клиента имя канала передается просто в аргументах приложения, но можно использовать и другие пути передачи, например анонимные каналы. Запись лога в файл сделана для удобства демонстрации работы клиента.
Пример клиента на Java
Клиент (впрочем, как и сервер) можно написать на любом языке программирования. Для примера, мы использовали Java для создания клиента и подключения к каналу, открытому сервером написанным на D.
import java.io.*;
public class Main {
public static void main(String[] args) {
String pipePath = "\\\\.\\pipe\\";
try (PrintStream log = new PrintStream(new FileOutputStream("client.log"))) {
System.setOut(log);
System.setErr(log);
if (args.length < 1) {
System.out.println("Usage: client <channel name>");
return;
}
String pipeName = pipePath + args[0];
try {
// Named pipe connection
try (RandomAccessFile pipeFile = new RandomAccessFile(pipeName, "rw")) {
System.out.println("Client: Connection established.");
// Reading a message from the server
String message = pipeFile.readLine();
System.out.println("Client: Message received - " + message);
// Отправка ответа серверу
pipeFile.writeBytes("hello parent\n");
System.out.println("Client: Response sent.");
}
} catch (IOException e) {
System.err.println("Error when working with named pipe:" + e.getMessage());
}
System.out.println("Client: Shut down.");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}Безопасность и шифрование
Обращаем ваше внимание на необходимость обеспечить безопасность передаваемых данных через каналы, особенно при обмене конфиденциальной информацией, особенно в POSIX системах, так как по умолчанию FIFO-каналы никак не защищены. Рекомендуем использовать шифрование данных, например, протокол (Elliptic Curve Diffie-Hellman) для обмена ключами и последующего шифрования передаваемой информации.
Заключение
Именованные каналы представляют собой эффективное средство для обмена данными между процессами. Они обеспечивают гибкость и безопасность при правильной реализации и использовании технологий шифрования. Кроме того, именованные каналы обеспечивают лучшее быстродействие по сравнению с большинством другим методов взаимодействия между процессами, особенно это касается IPC, который был написан специально для данной задачи.