Перейти к основному содержимому

Клиент на C++

Канал TCP/JSON не привязан к Python: подключиться к запущенной GAMMA можно из любой программы, умеющей работать с сокетами. Ниже целиком приведены два готовых клиента на C++ — их можно скопировать в файл, собрать и запустить, больше ничего не требуется.

Оба делают одно и то же: находят запущенное приложение, подключаются, выполняют по одному соединению команды Version и Help и печатают ответы. Различаются они только транспортом и способом поиска порта, поэтому их удобно читать рядом.

ВариантЧто нужно для сборкиКак ищет порт
на QtQt 5 или Qt 6 (QtCore, QtNetwork)перебирает порты 37000–37999 и берёт первый, который принял соединение
на WinSockтолько Windows SDK и компилятор Visual Studioчитает таблицу TCP системы и берёт слушающий порт на 127.0.0.1; ключ --pid сужает поиск до одного процесса

Вариант на WinSock не тянет ни одной сторонней библиотеки и запускается откуда угодно. Варианту на Qt нужны рядом Qt5Core/Qt5Network (или их аналоги для Qt 6) — иначе Windows не запустит программу, и вместо кода возврата вы увидите ошибку 0xC0000135.

примечание

Приложение должно быть уже запущено — обычным способом или без окна (Gamma.exe /invisible). Напомним: командный процессор обслуживает одного клиента за раз, а после разрыва соединения начинает слушать новый случайный порт, поэтому запомнить порт между сеансами нельзя.

Формат обмена

Поверх TCP лежит простое обрамление, одинаковое в обе стороны: 4 байта длины (старший байт первым), а за ними ровно столько байт JSON в кодировке UTF-8.

Запрос — объект из одного поля:

{"command":"Version"}

Ответ — объект из трёх:

{"result-data":"...","success-status":true,"error-message":""}

JSON в обоих примерах собирается и разбирается вручную, без сторонних библиотек: на таком формате это несколько десятков строк, зато пример остаётся самодостаточным. В своей программе, разумеется, проще взять готовый разбор JSON.

Коды возврата

Одинаковы у обеих программ:

КодЗначение
0обе команды выполнены
1неверные аргументы командной строки
2не удалось инициализировать сокеты (только вариант на WinSock)
3приложение не найдено, либо подключиться не удалось
4ошибка отправки запроса
5ответ не пришёл целиком за отведённое время
6испорченный ответ: не разбирается как JSON, либо пустой result-data
7команда выполнена, но вернула success-status: false
подсказка

Ожидание ответа ограничено ключом --timeout, по умолчанию это 60 000 мс. Расчёт в такое время не уложится — запуская команды вроде Solver Start, задавайте таймаут с запасом.

Вариант на Qt

TestConnectionQtSocket.exe [--host <адрес>] [--port <порт>] [--timeout <мс>]

Файл main.cpp:

// TestConnectionQtSocket - a standalone example of talking to a running
// Gamma application over the TCP/JSON channel of its command processor.
//
// The example depends on Qt alone - QtCore and QtNetwork - and on no header
// of the Gamma sources. TestConnectionWinSocket, next to it, does the very
// same thing on the bare Windows SDK; the two differ in their transport and
// in nothing else, which is the point of having both.
//
// The JSON is written and read by hand here. Qt does ship QJsonDocument, and
// in real code there would be no reason not to use it, but the format on
// this channel is small enough to spell out, and doing so keeps the example
// comparable with the WinAPI one line by line.
//
// The protocol, as implemented by CommandProcessorServer and TcpSocketJson:
// * the application listens on 127.0.0.1, on a random port taken from the
// range [37000, 37999];
// * every message, in both directions, is a 4-byte big-endian length
// followed by exactly that many bytes of UTF-8 JSON;
// * a request is an object with a single member:
// {"command":"Version"}
// * a reply is an object with three members:
// {"result-data":"...","success-status":true,"error-message":""}
// * only one client is served at a time: the moment a connection is
// accepted the listening socket is closed, and once that connection
// breaks the application starts listening again on a NEW random port.
// Both commands below therefore travel over one and the same
// connection - reconnecting in between would need a new port lookup.
//
// Usage:
// TestConnectionQtSocket.exe [--host <address>] [--port <port>]
// [--timeout <milliseconds>]
//
// Without --port the example walks the port range and takes the first port
// that accepts a connection: Qt offers no way to ask which process listens
// where, so unlike the WinAPI example it cannot tell two running
// applications apart. Pass --port when that matters.
//
// The process exits with 0 when both commands succeeded, and with one of the
// ExitCodes values below otherwise.

#include <cstdio>

#include <QByteArray>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QString>
#include <QStringList>
#include <QVector>
#include <QtNetwork/QTcpSocket>

namespace
{

//////////////////////////////////////////////////////////////////////////
// Codes this example returns to the caller, the same set and the same
// meaning as in TestConnectionWinSocket. A reply that arrived but does not
// carry the data it promises - a broken frame, JSON that does not parse, a
// missing or empty "result-data" - is reported as MalformedReply;
// CommandFailed means the application understood the command and refused it.
// SocketsInitFailed is listed to keep the two sets identical, but cannot
// happen here: Qt brings the socket layer up on its own.
namespace ExitCodes
{
enum Enum : int
{
Ok = 0,
BadArguments = 1,
SocketsInitFailed = 2,
ServerNotFound = 3,
SendFailed = 4,
NoReply = 5,
MalformedReply = 6,
CommandFailed = 7
};
}

constexpr quint16 PortsRangeFirst = 37000;
constexpr quint16 PortsRangeLast = 37999;
constexpr int DefaultTimeoutMs = 60000;
constexpr int ConnectTimeoutMs = 5000;
// While walking the range, a port nobody listens on is refused at once on
// the loopback interface, so this only has to cover a busy machine.
constexpr int ProbeTimeoutMs = 200;
constexpr int WriteTimeoutMs = 5000;

// A reply bigger than this is not something the command processor produces.
// Refusing it keeps a corrupted length header from turning into a huge
// allocation.
constexpr int MaxReplySize = 64 * 1024 * 1024;

//////////////////////////////////////////////////////////////////////////
struct Options
{
QString host{ QStringLiteral("127.0.0.1") };
quint16 port{ 0 };
int timeoutMs{ DefaultTimeoutMs };
};

//////////////////////////////////////////////////////////////////////////
// Written straight to stdout as UTF-8 instead of through a QTextStream: the
// call that sets the encoding of a stream was renamed between Qt 5 and Qt 6,
// and this way the example needs no version check at all.
void Print(const QString &text)
{
const QByteArray utf8 = text.toUtf8();
fwrite(utf8.constData(), 1, static_cast<size_t>(utf8.size()), stdout);
fflush(stdout);
}

//////////////////////////////////////////////////////////////////////////
void PrintUsage()
{
Print(QStringLiteral(
"Usage: TestConnectionQtSocket.exe [--host <address>] [--port <port>]\n"
" [--timeout <milliseconds>]\n"));
}

//////////////////////////////////////////////////////////////////////////
bool ParseArguments(const QStringList &arguments, Options &options)
{
for (int i = 1; i < arguments.size(); i++)
{
const QString argument = arguments.at(i);
const bool hasValue = (i + 1) < arguments.size();
bool isNumber = false;

if ((argument == QLatin1String("--host")) && hasValue)
{
options.host = arguments.at(++i);
}
else if ((argument == QLatin1String("--port")) && hasValue)
{
options.port = arguments.at(++i).toUShort(&isNumber);
if (!isNumber)
return false;
}
else if ((argument == QLatin1String("--timeout")) && hasValue)
{
options.timeoutMs = arguments.at(++i).toInt(&isNumber);
if (!isNumber)
return false;
}
else
{
return false;
}
}

return (options.timeoutMs > 0) && !options.host.isEmpty();
}

//////////////////////////////////////////////////////////////////////////
QString EscapeJsonString(const QString &text)
{
QString result;

for (int i = 0; i < text.size(); i++)
{
const QChar symbol = text.at(i);
switch (symbol.unicode())
{
case u'\"':
result += QLatin1String("\\\"");
break;
case u'\\':
result += QLatin1String("\\\\");
break;
case u'\b':
result += QLatin1String("\\b");
break;
case u'\f':
result += QLatin1String("\\f");
break;
case u'\n':
result += QLatin1String("\\n");
break;
case u'\r':
result += QLatin1String("\\r");
break;
case u'\t':
result += QLatin1String("\\t");
break;
default:
if (symbol.unicode() < 0x20)
{
result += QString::asprintf("\\u%04x",
static_cast<unsigned int>(symbol.unicode()));
}
else
{
result += symbol;
}
break;
}
}

return result;
}

//////////////////////////////////////////////////////////////////////////
QByteArray BuildRequest(const QString &command)
{
const QString request =
QStringLiteral("{\"command\":\"") + EscapeJsonString(command) +
QStringLiteral("\"}");

return request.toUtf8();
}

//////////////////////////////////////////////////////////////////////////
// One member of the replied object. Strings arrive unescaped in "value";
// everything else (true, false, null, a number) is kept as the raw token.
struct JsonMember
{
QString key;
QString value;
bool isString{ false };
};

//////////////////////////////////////////////////////////////////////////
void SkipSpaces(const QString &text, int &pos)
{
while (pos < text.size())
{
if (!text.at(pos).isSpace())
break;
pos++;
}
}

//////////////////////////////////////////////////////////////////////////
bool ReadHex4(const QString &text, int &pos, unsigned int &value)
{
if ((pos + 4) > text.size())
return false;

bool isNumber = false;
value = text.mid(pos, 4).toUInt(&isNumber, 16);
if (!isNumber)
return false;

pos += 4;

return true;
}

//////////////////////////////////////////////////////////////////////////
// Reads one JSON string starting at the opening quote and leaves "pos" just
// after the closing one. A surrogate pair needs no special handling here:
// QString holds UTF-16, so appending both escaped halves in turn produces
// the character they stand for.
bool ReadJsonString(const QString &text, int &pos, QString &value)
{
if ((pos >= text.size()) || (text.at(pos) != QLatin1Char('\"')))
return false;

pos++;
value.clear();

while (pos < text.size())
{
const QChar symbol = text.at(pos);
if (symbol == QLatin1Char('\"'))
{
pos++;
return true;
}

if (symbol != QLatin1Char('\\'))
{
value += symbol;
pos++;
continue;
}

pos++;
if (pos >= text.size())
return false;

const QChar escaped = text.at(pos);
pos++;

switch (escaped.unicode())
{
case u'\"':
value += QLatin1Char('\"');
break;
case u'\\':
value += QLatin1Char('\\');
break;
case u'/':
value += QLatin1Char('/');
break;
case u'b':
value += QLatin1Char('\b');
break;
case u'f':
value += QLatin1Char('\f');
break;
case u'n':
value += QLatin1Char('\n');
break;
case u'r':
value += QLatin1Char('\r');
break;
case u't':
value += QLatin1Char('\t');
break;
case u'u':
{
unsigned int codePoint = 0;
if (!ReadHex4(text, pos, codePoint))
return false;
value += QChar(static_cast<ushort>(codePoint));
break;
}
default:
return false;
}
}

return false;
}

//////////////////////////////////////////////////////////////////////////
// Reads a flat JSON object - which is all the reply ever is. A nested object
// or an array is rejected rather than skipped, so that a change of the
// protocol is noticed here instead of being quietly misread.
bool ParseFlatJsonObject(const QString &text, QVector<JsonMember> &members)
{
members.clear();

int pos = 0;
SkipSpaces(text, pos);
if ((pos >= text.size()) || (text.at(pos) != QLatin1Char('{')))
return false;

pos++;
SkipSpaces(text, pos);
if ((pos < text.size()) && (text.at(pos) == QLatin1Char('}')))
return true;

while (pos < text.size())
{
JsonMember member;

SkipSpaces(text, pos);
if (!ReadJsonString(text, pos, member.key))
return false;

SkipSpaces(text, pos);
if ((pos >= text.size()) || (text.at(pos) != QLatin1Char(':')))
return false;

pos++;
SkipSpaces(text, pos);
if (pos >= text.size())
return false;

if (text.at(pos) == QLatin1Char('\"'))
{
if (!ReadJsonString(text, pos, member.value))
return false;
member.isString = true;
}
else
{
if ((text.at(pos) == QLatin1Char('{')) ||
(text.at(pos) == QLatin1Char('[')))
{
return false;
}

const int start = pos;
while (pos < text.size())
{
const QChar symbol = text.at(pos);
if ((symbol == QLatin1Char(',')) || (symbol == QLatin1Char('}')))
break;
if (symbol.isSpace())
break;
pos++;
}

member.value = text.mid(start, pos - start);
if (member.value.isEmpty())
return false;
}

members.push_back(member);

SkipSpaces(text, pos);
if (pos >= text.size())
return false;

if (text.at(pos) == QLatin1Char(','))
{
pos++;
continue;
}

return text.at(pos) == QLatin1Char('}');
}

return false;
}

//////////////////////////////////////////////////////////////////////////
bool FindMember(const QVector<JsonMember> &members, const QString &key,
JsonMember &found)
{
for (int i = 0; i < members.size(); i++)
{
if (members.at(i).key == key)
{
found = members.at(i);
return true;
}
}

return false;
}

//////////////////////////////////////////////////////////////////////////
bool ParseReply(const QByteArray &reply, QString &resultData,
bool &successStatus, QString &errorMessage)
{
QVector<JsonMember> members;
if (!ParseFlatJsonObject(QString::fromUtf8(reply), members))
return false;

JsonMember member;

if (!FindMember(members, QStringLiteral("result-data"), member) ||
!member.isString)
{
return false;
}
resultData = member.value;

if (!FindMember(members, QStringLiteral("success-status"), member) ||
member.isString)
{
return false;
}
if (member.value == QLatin1String("true"))
successStatus = true;
else if (member.value == QLatin1String("false"))
successStatus = false;
else
return false;

errorMessage.clear();
if (FindMember(members, QStringLiteral("error-message"), member) &&
member.isString)
{
errorMessage = member.value;
}

return true;
}

//////////////////////////////////////////////////////////////////////////
bool SendFrame(QTcpSocket &socket, const QByteArray &payload)
{
const quint32 size = static_cast<quint32>(payload.size());

QByteArray frame;
frame.append(static_cast<char>((size >> 24) & 0xFF));
frame.append(static_cast<char>((size >> 16) & 0xFF));
frame.append(static_cast<char>((size >> 8) & 0xFF));
frame.append(static_cast<char>(size & 0xFF));
frame.append(payload);

if (socket.write(frame) != frame.size())
return false;

return socket.waitForBytesWritten(WriteTimeoutMs);
}

//////////////////////////////////////////////////////////////////////////
// Waits for exactly "size" bytes, giving up once the timer has run past the
// timeout. A command may compute for a long time, so the timeout is the
// caller's to choose - but there has to be one: without it a stopped
// application looks exactly like one that is still thinking.
bool ReceiveExactly(QTcpSocket &socket, int size, const QElapsedTimer &timer,
int timeoutMs, QByteArray &data)
{
data.clear();

while (data.size() < size)
{
if (socket.bytesAvailable() > 0)
{
data += socket.read(size - data.size());
continue;
}

const qint64 leftMs = timeoutMs - timer.elapsed();
if (leftMs <= 0)
return false;

if (!socket.waitForReadyRead(static_cast<int>(leftMs)))
return false;
}

return true;
}

//////////////////////////////////////////////////////////////////////////
bool ReceiveFrame(QTcpSocket &socket, int timeoutMs, QByteArray &payload)
{
QElapsedTimer timer;
timer.start();

QByteArray header;
if (!ReceiveExactly(socket, 4, timer, timeoutMs, header))
return false;

quint32 size = 0;
for (int i = 0; i < 4; i++)
{
size = (size << 8) | static_cast<quint8>(header.at(i));
}

if ((size == 0) || (size > static_cast<quint32>(MaxReplySize)))
return false;

return ReceiveExactly(socket, static_cast<int>(size), timer, timeoutMs,
payload);
}

//////////////////////////////////////////////////////////////////////////
bool ConnectToPort(QTcpSocket &socket, const QString &host, quint16 port,
int timeoutMs)
{
socket.abort();
socket.connectToHost(host, port);

return socket.waitForConnected(timeoutMs);
}

//////////////////////////////////////////////////////////////////////////
bool ConnectToApplication(QTcpSocket &socket, const Options &options)
{
if (options.port != 0)
{
const bool isConnected =
ConnectToPort(socket, options.host, options.port, ConnectTimeoutMs);
Print(QStringLiteral("%1 %2:%3.\n")
.arg(isConnected ? QStringLiteral("Connected to")
: QStringLiteral("Cannot connect to"))
.arg(options.host)
.arg(options.port));
return isConnected;
}

for (quint16 port = PortsRangeFirst; port <= PortsRangeLast; port++)
{
if (ConnectToPort(socket, options.host, port, ProbeTimeoutMs))
{
Print(QStringLiteral("Connected to %1:%2.\n")
.arg(options.host).arg(port));
return true;
}
}

Print(QStringLiteral(
"No application accepted a connection on ports %1-%2. It is either not "
"running, or already serving another client.\n")
.arg(PortsRangeFirst).arg(PortsRangeLast));

return false;
}

//////////////////////////////////////////////////////////////////////////
int RunCommand(QTcpSocket &socket, const QString &command, int timeoutMs)
{
if (!SendFrame(socket, BuildRequest(command)))
{
Print(QStringLiteral("Command \"%1\": sending the request failed.\n")
.arg(command));
return ExitCodes::SendFailed;
}

QByteArray reply;
if (!ReceiveFrame(socket, timeoutMs, reply))
{
Print(QStringLiteral("Command \"%1\": no complete reply within %2 ms.\n")
.arg(command).arg(timeoutMs));
return ExitCodes::NoReply;
}

QString resultData;
QString errorMessage;
bool successStatus = false;
if (!ParseReply(reply, resultData, successStatus, errorMessage))
{
Print(QStringLiteral(
"Command \"%1\": the reply is not the expected JSON object: %2\n")
.arg(command, QString::fromUtf8(reply)));
return ExitCodes::MalformedReply;
}

if (!successStatus)
{
Print(QStringLiteral("Command \"%1\" failed: %2\n")
.arg(command, errorMessage.isEmpty()
? QStringLiteral("<no error message>") : errorMessage));
return ExitCodes::CommandFailed;
}

if (resultData.isEmpty())
{
Print(QStringLiteral("Command \"%1\" succeeded but returned no data.\n")
.arg(command));
return ExitCodes::MalformedReply;
}

Print(QStringLiteral("Command \"%1\" succeeded. Result:\n%2\n")
.arg(command, resultData));

return ExitCodes::Ok;
}

} // namespace

//////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[])
{
const QCoreApplication application(argc, argv);

Options options;
if (!ParseArguments(QCoreApplication::arguments(), options))
{
PrintUsage();
return ExitCodes::BadArguments;
}

int result = ExitCodes::Ok;

QTcpSocket socket;
if (!ConnectToApplication(socket, options))
{
result = ExitCodes::ServerNotFound;
}
else
{
// Both commands go over this one connection on purpose: closing it makes
// the application listen on a different port again.
result = RunCommand(socket, QStringLiteral("Version"), options.timeoutMs);
if (result == ExitCodes::Ok)
result = RunCommand(socket, QStringLiteral("Help"), options.timeoutMs);

socket.disconnectFromHost();
}

Print(QStringLiteral("Exit code: %1\n").arg(result));

return result;
}

Файл CMakeLists.txt рядом с ним:

cmake_minimum_required(VERSION 3.20)
project(TestConnectionQtSocket LANGUAGES CXX)

# Если Qt установлен не в системный каталог, укажите путь к нему
# в переменной окружения QT_DIR.
if(DEFINED ENV{QT_DIR} AND NOT "$ENV{QT_DIR}" STREQUAL "")
list(APPEND CMAKE_PREFIX_PATH "$ENV{QT_DIR}/")
endif()

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network)

add_executable(TestConnectionQtSocket main.cpp)

set_target_properties(TestConnectionQtSocket PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)

# AUTOMOC не нужен: своих классов QObject в примере нет.
target_link_libraries(TestConnectionQtSocket PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
)

Сборка:

cmake -S . -B build -A x64
cmake --build build --config Release

Вариант на WinSock

TestConnectionWinSocket.exe [--host <адрес>] [--port <порт>] [--pid <id>] [--timeout <мс>]

Идентификатор процесса для --pid виден в диспетчере задач; из Python его даёт gamma.service.instances().

Файл main.cpp:

// TestConnectionWinSocket - a standalone example of talking to a running
// Gamma application over the TCP/JSON channel of its command processor.
//
// The example depends on nothing but the Windows SDK: no Qt, no JSON
// library, no header of the Gamma sources. That is deliberate - it is meant
// to document the wire protocol on its own, not to be a part of the
// application. For the same reason the JSON is written and read by hand
// here; the format used on this channel is small enough for that, and it
// keeps the example free of third-party code.
//
// The protocol, as implemented by CommandProcessorServer and TcpSocketJson:
// * the application listens on 127.0.0.1, on a random port taken from the
// range [37000, 37999];
// * every message, in both directions, is a 4-byte big-endian length
// followed by exactly that many bytes of UTF-8 JSON;
// * a request is an object with a single member:
// {"command":"Version"}
// * a reply is an object with three members:
// {"result-data":"...","success-status":true,"error-message":""}
// * only one client is served at a time: the moment a connection is
// accepted the listening socket is closed, and once that connection
// breaks the application starts listening again on a NEW random port.
// Both commands below therefore travel over one and the same
// connection - reconnecting in between would need a new port lookup.
//
// Usage:
// TestConnectionWinSocket.exe [--host <address>] [--port <port>]
// [--pid <process id>]
// [--timeout <milliseconds>]
//
// Without --port the port is looked up in the TCP table of the system, the
// same way the Python integration (Sources/GammaApi) finds it. --pid narrows
// that lookup to one process, which is what tells two running applications
// apart.
//
// The process exits with 0 when both commands succeeded, and with one of the
// ExitCodes values below otherwise.

#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#include <string>
#include <vector>

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>

#ifdef _MSC_VER
// Stated here as well as in CMakeLists.txt so that the file also builds on
// its own, with a bare "cl main.cpp".
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Iphlpapi.lib")
#endif

namespace
{

//////////////////////////////////////////////////////////////////////////
// Codes this example returns to the caller. A reply that arrived but does
// not carry the data it promises - a broken frame, JSON that does not parse,
// a missing or empty "result-data" - is reported as MalformedReply;
// CommandFailed means the application understood the command and refused it.
namespace ExitCodes
{
enum Enum : int
{
Ok = 0,
BadArguments = 1,
SocketsInitFailed = 2,
ServerNotFound = 3,
SendFailed = 4,
NoReply = 5,
MalformedReply = 6,
CommandFailed = 7
};
}

constexpr unsigned short PortsRangeFirst = 37000;
constexpr unsigned short PortsRangeLast = 37999;
constexpr int DefaultTimeoutMs = 60000;
constexpr const char *DefaultHost = "127.0.0.1";

// A reply bigger than this is not something the command processor produces.
// Refusing it keeps a corrupted length header from turning into a huge
// allocation.
constexpr unsigned int MaxReplySize = 64u * 1024u * 1024u;

//////////////////////////////////////////////////////////////////////////
struct Options
{
std::string host{ DefaultHost };
unsigned short port{ 0 };
unsigned int processId{ 0 };
int timeoutMs{ DefaultTimeoutMs };
};

//////////////////////////////////////////////////////////////////////////
void PrintUsage()
{
printf(
"Usage: TestConnectionWinSocket.exe [--host <address>] [--port <port>]\n"
" [--pid <process id>]\n"
" [--timeout <milliseconds>]\n");
}

//////////////////////////////////////////////////////////////////////////
bool ParseArguments(int argc, char *argv[], Options &options)
{
for (int i = 1; i < argc; i++)
{
const std::string argument = argv[i];
const bool hasValue = (i + 1) < argc;

if ((argument == "--host") && hasValue)
{
options.host = argv[++i];
}
else if ((argument == "--port") && hasValue)
{
options.port = static_cast<unsigned short>(std::atoi(argv[++i]));
}
else if ((argument == "--pid") && hasValue)
{
options.processId = static_cast<unsigned int>(std::atoi(argv[++i]));
}
else if ((argument == "--timeout") && hasValue)
{
options.timeoutMs = std::atoi(argv[++i]);
}
else
{
return false;
}
}

return (options.timeoutMs > 0) && !options.host.empty();
}

//////////////////////////////////////////////////////////////////////////
void AppendUtf8(std::string &text, unsigned int codePoint)
{
if (codePoint < 0x80)
{
text += static_cast<char>(codePoint);
}
else if (codePoint < 0x800)
{
text += static_cast<char>(0xC0 | (codePoint >> 6));
text += static_cast<char>(0x80 | (codePoint & 0x3F));
}
else if (codePoint < 0x10000)
{
text += static_cast<char>(0xE0 | (codePoint >> 12));
text += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
text += static_cast<char>(0x80 | (codePoint & 0x3F));
}
else
{
text += static_cast<char>(0xF0 | (codePoint >> 18));
text += static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
text += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
text += static_cast<char>(0x80 | (codePoint & 0x3F));
}
}

//////////////////////////////////////////////////////////////////////////
std::string EscapeJsonString(const std::string &text)
{
std::string result;

for (size_t i = 0; i < text.size(); i++)
{
const char symbol = text[i];
switch (symbol)
{
case '\"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\b':
result += "\\b";
break;
case '\f':
result += "\\f";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
if (static_cast<unsigned char>(symbol) < 0x20)
{
char buffer[8] = { 0 };
sprintf_s(buffer, sizeof(buffer), "\\u%04x",
static_cast<unsigned int>(static_cast<unsigned char>(symbol)));
result += buffer;
}
else
{
result += symbol;
}
break;
}
}

return result;
}

//////////////////////////////////////////////////////////////////////////
std::string BuildRequest(const std::string &command)
{
return std::string("{\"command\":\"") + EscapeJsonString(command) + "\"}";
}

//////////////////////////////////////////////////////////////////////////
// One member of the replied object. Strings arrive unescaped in "value";
// everything else (true, false, null, a number) is kept as the raw token.
struct JsonMember
{
std::string key;
std::string value;
bool isString{ false };
};

//////////////////////////////////////////////////////////////////////////
void SkipSpaces(const std::string &text, size_t &pos)
{
while (pos < text.size())
{
if (std::isspace(static_cast<unsigned char>(text[pos])) == 0)
break;
pos++;
}
}

//////////////////////////////////////////////////////////////////////////
bool ReadHex4(const std::string &text, size_t &pos, unsigned int &value)
{
if ((pos + 4) > text.size())
return false;

value = 0;
for (size_t i = 0; i < 4; i++)
{
const char symbol = text[pos + i];
unsigned int digit = 0;
if ((symbol >= '0') && (symbol <= '9'))
digit = static_cast<unsigned int>(symbol - '0');
else if ((symbol >= 'a') && (symbol <= 'f'))
digit = static_cast<unsigned int>(symbol - 'a') + 10;
else if ((symbol >= 'A') && (symbol <= 'F'))
digit = static_cast<unsigned int>(symbol - 'A') + 10;
else
return false;

value = (value << 4) | digit;
}

pos += 4;
return true;
}

//////////////////////////////////////////////////////////////////////////
// Reads one JSON string starting at the opening quote and leaves "pos" just
// after the closing one. Both escape forms are handled: the short ones and
// \uXXXX, including a surrogate pair, which is turned back into UTF-8.
bool ReadJsonString(const std::string &text, size_t &pos, std::string &value)
{
if ((pos >= text.size()) || (text[pos] != '\"'))
return false;

pos++;
value.clear();

while (pos < text.size())
{
const char symbol = text[pos];
if (symbol == '\"')
{
pos++;
return true;
}

if (symbol != '\\')
{
value += symbol;
pos++;
continue;
}

pos++;
if (pos >= text.size())
return false;

const char escaped = text[pos];
pos++;

switch (escaped)
{
case '\"':
value += '\"';
break;
case '\\':
value += '\\';
break;
case '/':
value += '/';
break;
case 'b':
value += '\b';
break;
case 'f':
value += '\f';
break;
case 'n':
value += '\n';
break;
case 'r':
value += '\r';
break;
case 't':
value += '\t';
break;
case 'u':
{
unsigned int codePoint = 0;
if (!ReadHex4(text, pos, codePoint))
return false;

// A character outside the basic plane is written as two escapes:
// a high surrogate followed by a low one. Taken apart they are not
// valid characters, so they are only useful together.
const bool isHighSurrogate =
(codePoint >= 0xD800) && (codePoint <= 0xDBFF);
if (isHighSurrogate && ((pos + 1) < text.size()) &&
(text[pos] == '\\') && (text[pos + 1] == 'u'))
{
size_t lowPos = pos + 2;
unsigned int lowSurrogate = 0;
if (ReadHex4(text, lowPos, lowSurrogate) &&
(lowSurrogate >= 0xDC00) && (lowSurrogate <= 0xDFFF))
{
codePoint = 0x10000 + ((codePoint - 0xD800) << 10) +
(lowSurrogate - 0xDC00);
pos = lowPos;
}
}

AppendUtf8(value, codePoint);
break;
}
default:
return false;
}
}

return false;
}

//////////////////////////////////////////////////////////////////////////
// Reads a flat JSON object - which is all the reply ever is. A nested object
// or an array is rejected rather than skipped, so that a change of the
// protocol is noticed here instead of being quietly misread.
bool ParseFlatJsonObject(const std::string &text,
std::vector<JsonMember> &members)
{
members.clear();

size_t pos = 0;
SkipSpaces(text, pos);
if ((pos >= text.size()) || (text[pos] != '{'))
return false;

pos++;
SkipSpaces(text, pos);
if ((pos < text.size()) && (text[pos] == '}'))
return true;

while (pos < text.size())
{
JsonMember member;

SkipSpaces(text, pos);
if (!ReadJsonString(text, pos, member.key))
return false;

SkipSpaces(text, pos);
if ((pos >= text.size()) || (text[pos] != ':'))
return false;

pos++;
SkipSpaces(text, pos);
if (pos >= text.size())
return false;

if (text[pos] == '\"')
{
if (!ReadJsonString(text, pos, member.value))
return false;
member.isString = true;
}
else
{
if ((text[pos] == '{') || (text[pos] == '['))
return false;

const size_t start = pos;
while (pos < text.size())
{
const char symbol = text[pos];
if ((symbol == ',') || (symbol == '}'))
break;
if (std::isspace(static_cast<unsigned char>(symbol)) != 0)
break;
pos++;
}

member.value = text.substr(start, pos - start);
if (member.value.empty())
return false;
}

members.push_back(member);

SkipSpaces(text, pos);
if (pos >= text.size())
return false;

if (text[pos] == ',')
{
pos++;
continue;
}

return text[pos] == '}';
}

return false;
}

//////////////////////////////////////////////////////////////////////////
bool FindMember(const std::vector<JsonMember> &members,
const std::string &key, JsonMember &found)
{
for (size_t i = 0; i < members.size(); i++)
{
if (members[i].key == key)
{
found = members[i];
return true;
}
}

return false;
}

//////////////////////////////////////////////////////////////////////////
bool ParseReply(const std::string &json, std::string &resultData,
bool &successStatus, std::string &errorMessage)
{
std::vector<JsonMember> members;
if (!ParseFlatJsonObject(json, members))
return false;

JsonMember member;

if (!FindMember(members, "result-data", member) || !member.isString)
return false;
resultData = member.value;

if (!FindMember(members, "success-status", member) || member.isString)
return false;
if (member.value == "true")
successStatus = true;
else if (member.value == "false")
successStatus = false;
else
return false;

errorMessage.clear();
if (FindMember(members, "error-message", member) && member.isString)
errorMessage = member.value;

return true;
}

//////////////////////////////////////////////////////////////////////////
// send() is free to take less than it is offered - a full send buffer is
// reason enough - so it is called until everything is gone.
bool SendAll(SOCKET connection, const char *pData, size_t size)
{
size_t sent = 0;
while (sent < size)
{
const int sentNow = send(connection, pData + sent,
static_cast<int>(size - sent), 0);
if (sentNow <= 0)
return false;
sent += static_cast<size_t>(sentNow);
}

return true;
}

//////////////////////////////////////////////////////////////////////////
bool SendFrame(SOCKET connection, const std::string &payload)
{
const unsigned int size = static_cast<unsigned int>(payload.size());

char header[4] = { 0 };
header[0] = static_cast<char>((size >> 24) & 0xFF);
header[1] = static_cast<char>((size >> 16) & 0xFF);
header[2] = static_cast<char>((size >> 8) & 0xFF);
header[3] = static_cast<char>(size & 0xFF);

if (!SendAll(connection, header, sizeof(header)))
return false;

return SendAll(connection, payload.data(), payload.size());
}

//////////////////////////////////////////////////////////////////////////
// Waits for exactly "size" bytes, giving up at "deadline". A command may
// compute for a long time, so the timeout is the caller's to choose - but
// there has to be one: without it a stopped application looks exactly like
// one that is still thinking.
bool ReceiveExactly(SOCKET connection, size_t size, ULONGLONG deadline,
std::string &data)
{
data.clear();

while (data.size() < size)
{
const ULONGLONG now = GetTickCount64();
if (now >= deadline)
return false;

const ULONGLONG leftMs = deadline - now;

fd_set readSet;
FD_ZERO(&readSet);
FD_SET(connection, &readSet);

timeval timeout;
timeout.tv_sec = static_cast<long>(leftMs / 1000);
timeout.tv_usec = static_cast<long>((leftMs % 1000) * 1000);

if (select(0, &readSet, nullptr, nullptr, &timeout) <= 0)
return false;

char buffer[4096] = { 0 };
const size_t wanted = size - data.size();
const size_t chunk = (wanted < sizeof(buffer)) ? wanted : sizeof(buffer);

const int received =
recv(connection, buffer, static_cast<int>(chunk), 0);
if (received <= 0)
return false;

data.append(buffer, static_cast<size_t>(received));
}

return true;
}

//////////////////////////////////////////////////////////////////////////
bool ReceiveFrame(SOCKET connection, int timeoutMs, std::string &payload)
{
const ULONGLONG deadline =
GetTickCount64() + static_cast<ULONGLONG>(timeoutMs);

std::string header;
if (!ReceiveExactly(connection, 4, deadline, header))
return false;

unsigned int size = 0;
for (size_t i = 0; i < 4; i++)
{
size = (size << 8) | static_cast<unsigned char>(header[i]);
}

if ((size == 0) || (size > MaxReplySize))
return false;

return ReceiveExactly(connection, size, deadline, payload);
}

//////////////////////////////////////////////////////////////////////////
// Collects every port of the command processor range that is being listened
// on at 127.0.0.1. When processId is not zero only that process is looked
// at, which is what tells two running applications apart.
bool FindListeningPorts(unsigned int processId,
std::vector<unsigned short> &ports)
{
ports.clear();

std::vector<unsigned char> buffer;
DWORD size = 0;

// The table can grow between the call that asks how much room it needs and
// the one that reads it, hence the retry; the count bounds it.
constexpr int MaxAttempts = 5;
for (int attempt = 0; attempt < MaxAttempts; attempt++)
{
// The only allocation of a size the example does not choose itself, so
// the only one worth guarding - throwing out of here is not an option.
try
{
buffer.resize(size, 0);
}
catch (...)
{
return false;
}

MIB_TCPTABLE_OWNER_PID *pTable = (size > 0)
? reinterpret_cast<MIB_TCPTABLE_OWNER_PID *>(buffer.data())
: nullptr;

const DWORD status = GetExtendedTcpTable(pTable, &size, FALSE, AF_INET,
TCP_TABLE_OWNER_PID_ALL, 0);
if (status == ERROR_INSUFFICIENT_BUFFER)
continue;
if (status != NO_ERROR)
return false;
if (pTable == nullptr)
return false;

for (DWORD i = 0; i < pTable->dwNumEntries; i++)
{
const MIB_TCPROW_OWNER_PID &row = pTable->table[i];
if (row.dwState != MIB_TCP_STATE_LISTEN)
continue;
if (row.dwLocalAddr != htonl(INADDR_LOOPBACK))
continue;
if ((processId != 0) && (row.dwOwningPid != processId))
continue;

const unsigned short port =
ntohs(static_cast<unsigned short>(row.dwLocalPort));
if ((port >= PortsRangeFirst) && (port <= PortsRangeLast))
ports.push_back(port);
}

return true;
}

return false;
}

//////////////////////////////////////////////////////////////////////////
SOCKET ConnectToPort(const std::string &host, unsigned short port)
{
addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

const std::string portText = std::to_string(port);
addrinfo *pInfo = nullptr;
if (getaddrinfo(host.c_str(), portText.c_str(), &hints, &pInfo) != 0)
return INVALID_SOCKET;

SOCKET connection = INVALID_SOCKET;
for (addrinfo *pEntry = pInfo; pEntry != nullptr; pEntry = pEntry->ai_next)
{
connection = socket(pEntry->ai_family, pEntry->ai_socktype,
pEntry->ai_protocol);
if (connection == INVALID_SOCKET)
continue;

if (connect(connection, pEntry->ai_addr,
static_cast<int>(pEntry->ai_addrlen)) == 0)
{
break;
}

closesocket(connection);
connection = INVALID_SOCKET;
}

freeaddrinfo(pInfo);

return connection;
}

//////////////////////////////////////////////////////////////////////////
SOCKET ConnectToApplication(const Options &options)
{
if (options.port != 0)
{
const SOCKET connection = ConnectToPort(options.host, options.port);
if (connection == INVALID_SOCKET)
printf("Cannot connect to %s:%u.\n", options.host.c_str(),
static_cast<unsigned int>(options.port));
else
printf("Connected to %s:%u.\n", options.host.c_str(),
static_cast<unsigned int>(options.port));
return connection;
}

std::vector<unsigned short> ports;
if (!FindListeningPorts(options.processId, ports))
{
printf("Cannot read the TCP table of the system.\n");
return INVALID_SOCKET;
}

if (ports.empty())
{
printf("No application is listening on ports %u-%u. It is either not "
"running, or already serving another client.\n",
static_cast<unsigned int>(PortsRangeFirst),
static_cast<unsigned int>(PortsRangeLast));
return INVALID_SOCKET;
}

for (size_t i = 0; i < ports.size(); i++)
{
const SOCKET connection = ConnectToPort(options.host, ports[i]);
if (connection != INVALID_SOCKET)
{
printf("Connected to %s:%u.\n", options.host.c_str(),
static_cast<unsigned int>(ports[i]));
return connection;
}
}

printf("None of the found ports accepted a connection.\n");

return INVALID_SOCKET;
}

//////////////////////////////////////////////////////////////////////////
int RunCommand(SOCKET connection, const std::string &command, int timeoutMs)
{
if (!SendFrame(connection, BuildRequest(command)))
{
printf("Command \"%s\": sending the request failed.\n", command.c_str());
return ExitCodes::SendFailed;
}

std::string reply;
if (!ReceiveFrame(connection, timeoutMs, reply))
{
printf("Command \"%s\": no complete reply within %d ms.\n",
command.c_str(), timeoutMs);
return ExitCodes::NoReply;
}

std::string resultData;
std::string errorMessage;
bool successStatus = false;
if (!ParseReply(reply, resultData, successStatus, errorMessage))
{
printf("Command \"%s\": the reply is not the expected JSON object: %s\n",
command.c_str(), reply.c_str());
return ExitCodes::MalformedReply;
}

if (!successStatus)
{
printf("Command \"%s\" failed: %s\n", command.c_str(),
errorMessage.empty() ? "<no error message>" : errorMessage.c_str());
return ExitCodes::CommandFailed;
}

if (resultData.empty())
{
printf("Command \"%s\" succeeded but returned no data.\n",
command.c_str());
return ExitCodes::MalformedReply;
}

printf("Command \"%s\" succeeded. Result:\n%s\n", command.c_str(),
resultData.c_str());

return ExitCodes::Ok;
}

} // namespace

//////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[])
{
Options options;
if (!ParseArguments(argc, argv, options))
{
PrintUsage();
return ExitCodes::BadArguments;
}

WSADATA wsaData;
memset(&wsaData, 0, sizeof(wsaData));
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Winsock initialization failed.\n");
return ExitCodes::SocketsInitFailed;
}

int result = ExitCodes::Ok;

const SOCKET connection = ConnectToApplication(options);
if (connection == INVALID_SOCKET)
{
result = ExitCodes::ServerNotFound;
}
else
{
// Both commands go over this one connection on purpose: closing it makes
// the application listen on a different port again.
result = RunCommand(connection, "Version", options.timeoutMs);
if (result == ExitCodes::Ok)
result = RunCommand(connection, "Help", options.timeoutMs);

shutdown(connection, SD_BOTH);
closesocket(connection);
}

WSACleanup();

printf("Exit code: %d\n", result);

return result;
}

Нужные библиотеки названы в самом файле через #pragma comment, поэтому в командной строке разработчика Visual Studio достаточно одной команды:

cl /EHsc /std:c++17 main.cpp

Если удобнее CMake, положите рядом такой CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(TestConnectionWinSocket LANGUAGES CXX)

add_executable(TestConnectionWinSocket main.cpp)

set_target_properties(TestConnectionWinSocket PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)

target_link_libraries(TestConnectionWinSocket PRIVATE Ws2_32 Iphlpapi)

и соберите теми же двумя командами, что и вариант на Qt.

Дальше