I would like to implement full duplex named pipe communications on Windows. The server needs to be in C++, and needs to be able to connect to multiple clients simultaneously. The client needs to be in C#. The client/server languages are not negotiable, this is part of an existing project.
This is what I have currently as a demo app to try and provide a POC. This is the C++ server side:
#include <windows.h>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
// Handle client communication
void handleClientCommunication(HANDLE hPipe) {
char buffer[512];
DWORD bytesRead, bytesWritten;
BOOL result;
while (true) {
// Read from client
result = ReadFile(hPipe, buffer, sizeof(buffer), &bytesRead, NULL);
if (!result || bytesRead == 0) {
if (GetLastError() == ERROR_BROKEN_PIPE) {
std::cout << "Client disconnected." << std::endl;
} else {
std::cerr << "ReadFile failed, GLE=" << GetLastError() << std::endl;
}
break;
}
std::string receivedMessage(buffer, bytesRead);
std::cout << "Received: " << receivedMessage << std::endl;
// Echo the message back to the client
result = WriteFile(hPipe, buffer, bytesRead, &bytesWritten, NULL);
if (!result || bytesWritten != bytesRead) {
std::cerr << "WriteFile failed, GLE=" << GetLastError() << std::endl;
break;
}
}
CloseHandle(hPipe);
}
// Read server input and send messages to client
void readServerInputAndSendMessages(HANDLE hPipe) {
std::string input;
DWORD bytesWritten;
while (true) {
std::getline(std::cin, input); // Read input from server's keyboard
input += 'n'; // Optionally add a newline character
// Send message to client
BOOL result = WriteFile(hPipe, input.c_str(), input.size(), &bytesWritten, NULL);
if (!result || bytesWritten != input.size()) {
std::cerr << "WriteFile failed (sending from server), GLE=" << GetLastError() << std::endl;
break;
}
}
}
// Create named pipe server
void createNamedPipeServer() {
const std::wstring pipeName = L"\\.\pipe\MyNamedPipe";
std::vector<std::thread> threads;
while (true) {
HANDLE hPipe = CreateNamedPipe(
pipeName.c_str(), // pipe name
PIPE_ACCESS_DUPLEX, // read/write access
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
PIPE_UNLIMITED_INSTANCES, // max. instances
512, // output buffer size
512, // input buffer size
0, // client time-out
NULL); // default security attribute
if (hPipe == INVALID_HANDLE_VALUE) {
std::cerr << "CreateNamedPipe failed, GLE=" << GetLastError() << std::endl;
return;
}
std::cout << "Waiting for client connection..." << std::endl;
BOOL connected = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (connected) {
std::cout << "Client connected, creating communication threads." << std::endl;
// Start thread to handle client communication
std::thread clientThread(handleClientCommunication, hPipe);
clientThread.detach();
// Start thread to read server input and send messages to client
std::thread inputThread(readServerInputAndSendMessages, hPipe);
inputThread.detach();
} else {
CloseHandle(hPipe);
}
}
}
int main() {
createNamedPipeServer();
return 0;
}
This is the C# client:
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace PipeClient
{
class Program
{
static void Main(string[] args)
{
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "MyNamedPipe", PipeDirection.InOut, PipeOptions.Asynchronous))
{
Console.WriteLine("Connecting to server...n");
pipeClient.Connect();
CancellationTokenSource cts = new CancellationTokenSource();
// Start sending and receiving tasks
Task sendTask = Task.Run(() => SendMessages(pipeClient, cts.Token));
Task receiveTask = Task.Run(() => ReceiveMessages(pipeClient, cts.Token));
Task.WhenAny(sendTask, receiveTask).Wait();
// Cancel the other task and wait for it to complete
cts.Cancel();
Task.WaitAll(new[] { sendTask, receiveTask }, TimeSpan.FromSeconds(2));
}
Console.WriteLine("closed pipe");
}
static async Task SendMessages(NamedPipeClientStream pipeClient, CancellationToken token)
{
using (StreamWriter sw = new StreamWriter(pipeClient))
{
sw.AutoFlush = true;
while (!token.IsCancellationRequested)
{
Console.Write("Enter message (or 'exit' to quit): ");
string message = Console.ReadLine();
if (message == "exit")
break;
await sw.WriteLineAsync(message);
}
}
}
static async Task ReceiveMessages(NamedPipeClientStream pipeClient, CancellationToken token)
{
using (StreamReader sr = new StreamReader(pipeClient))
{
while (!token.IsCancellationRequested)
{
try
{
string response = await sr.ReadLineAsync();
if (response == null)
break;
Console.WriteLine("Received from server: " + response);
}
catch (IOException)
{
break; // Exit the loop if the pipe is closed
}
}
}
}
}
}
Everything is working as intended, (though I have not tested multiple clients, I am sure that will need more work), except the server won’t send a message unless it first receives one from the client. The message from the server seems to be cached till the client initiates a connection, then the ‘cached’ data is returned along with the echoed response from the clients message.
I am trying to figure out if the issue is indeed a stream caching issue that can be resolved, or if my whole named pipe connection method is flawed.