Клиент на C++
Канал TCP/JSON не привязан к Python: подключиться к запущенной GAMMA можно из любой программы, умеющей работать с сокетами. Ниже целиком приведены два готовых клиента на C++ — их можно скопировать в файл, собрать и запустить, больше ничего не требуется.
Оба делают одно и то же: находят запущенное приложение, подключаются,
выполняют по одному соединению команды Version и Help и печатают ответы.
Различаются они только транспортом и способом поиска порта, поэтому их удобно
читать рядом.
| Вариант | Что нужно для сборки | Как ищет порт |
|---|---|---|
| на Qt | Qt 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.
Дальше
- Примеры — сценарии и Python-скрипты целиком;
- Если что-то не работает — что делать, когда подключиться не удаётся.