예제 소스
인베이더 게임으로 배우는 휴리스틱과 AI 브리지 · 파일 17개
tcpvlmclient.cpp
#include "tcpvlmclient.h"
#include "gameengine.h"
#include <QAbstractSocket>
#include <QBuffer>
#include <QDataStream>
#include <QElapsedTimer>
#include <QTcpSocket>
namespace TcpVlmClient {
namespace {
void packU32BE(QByteArray *out, quint32 v)
{
out->append(char((v >> 24) & 0xff));
out->append(char((v >> 16) & 0xff));
out->append(char((v >> 8) & 0xff));
out->append(char(v & 0xff));
}
bool validGrid(int rows, int cols)
{
return rows == Invader::kGridRows && cols == Invader::kGridCols;
}
} // namespace
Worker::Worker(QObject *parent)
: QObject(parent)
{
}
Worker::~Worker()
{
closeSocketLocked(QStringLiteral("worker destroyed"));
}
bool Worker::isBusy() const
{
return m_busy.loadAcquire() != 0;
}
bool Worker::isConnected() const
{
return m_socket && m_socket->state() == QAbstractSocket::ConnectedState;
}
void Worker::emitConnState()
{
QString endpoint;
bool connected = false;
if (m_socket && m_socket->state() == QAbstractSocket::ConnectedState) {
endpoint = QStringLiteral("%1:%2").arg(m_curHost).arg(m_curPort);
connected = true;
}
emit connectionStateChanged(connected, endpoint);
}
void Worker::closeSocketLocked(const QString &reason)
{
Q_UNUSED(reason);
if (m_socket) {
m_socket->abort();
m_socket->deleteLater();
m_socket = nullptr;
}
m_curHost.clear();
m_curPort = 0;
emitConnState();
}
void Worker::cancel()
{
m_cancel.storeRelease(1);
}
void Worker::closeConnection()
{
closeSocketLocked(QStringLiteral("explicit close"));
}
bool Worker::ensureConnected(const QString &host, quint16 port, int timeoutMs, QString *errorOut)
{
if (errorOut)
errorOut->clear();
if (m_socket && m_socket->state() == QAbstractSocket::ConnectedState && m_curHost == host
&& m_curPort == port) {
return true;
}
if (m_socket)
closeSocketLocked(QStringLiteral("reconnect"));
m_socket = new QTcpSocket();
m_socket->connectToHost(host, port);
const int connTo = qBound(500, timeoutMs, 15000);
if (!m_socket->waitForConnected(connTo)) {
if (errorOut)
*errorOut = QStringLiteral("TCP 연결 실패: %1").arg(m_socket->errorString());
closeSocketLocked(QStringLiteral("connect fail"));
return false;
}
m_curHost = host;
m_curPort = port;
emitConnState();
return true;
}
bool Worker::writeAll(const QByteArray &data, int timeoutMs)
{
if (!m_socket || m_socket->state() != QAbstractSocket::ConnectedState)
return false;
m_socket->write(data);
while (m_socket->bytesToWrite() > 0) {
if (m_cancel.loadAcquire() != 0)
return false;
if (!m_socket->waitForBytesWritten(qMax(200, timeoutMs)))
return false;
}
return true;
}
bool Worker::readBytes(QByteArray *dst, qint64 nbytes, int deadlineMs)
{
if (!dst)
return false;
dst->clear();
if (!m_socket)
return false;
QElapsedTimer deadline;
deadline.start();
while (dst->size() < nbytes) {
if (m_cancel.loadAcquire() != 0)
return false;
if (!m_socket)
return false;
if (m_socket->bytesAvailable() > 0) {
QByteArray chunk = m_socket->read(nbytes - dst->size());
if (!chunk.isEmpty())
dst->append(chunk);
if (dst->size() >= nbytes)
return true;
continue;
}
if (m_socket->state() != QAbstractSocket::ConnectedState)
return false;
const qint64 elapsed = deadline.elapsed();
if (elapsed >= deadlineMs)
return false;
const qint64 remaining = deadlineMs - elapsed;
qint64 slice64 = remaining;
if (slice64 > 200)
slice64 = 200;
if (slice64 < 50)
slice64 = 50;
(void)m_socket->waitForReadyRead(int(slice64));
}
return true;
}
void Worker::doPing(QString host, quint16 port, int timeoutMs)
{
m_cancel.storeRelease(0);
QString err;
if (!ensureConnected(host, port, timeoutMs, &err)) {
emit pingReady(false, err);
return;
}
if (!writeAll(QByteArrayLiteral("PING"), timeoutMs)) {
const QString e = m_socket ? m_socket->errorString() : QStringLiteral("PING 송신 실패");
closeSocketLocked(QStringLiteral("ping write fail"));
emit pingReady(false, e);
return;
}
QByteArray pong;
if (!readBytes(&pong, 4, timeoutMs) || pong != QByteArrayLiteral("PONG")) {
closeSocketLocked(QStringLiteral("pong recv fail"));
emit pingReady(false, QStringLiteral("PONG 응답 없음/불일치"));
return;
}
emit pingReady(true, QString());
}
void Worker::requestMove(QImage boardImage, QByteArray boardPacked, int rows, int cols, QString host,
quint16 port, int timeoutMs)
{
m_cancel.storeRelease(0);
m_busy.storeRelease(1);
auto fail = [&](const QString &msg) {
m_busy.storeRelease(0);
emit moveReady(false, -1, -1, msg);
};
if (!validGrid(rows, cols)) {
fail(QStringLiteral("격자 크기 오류 (%1×%2)").arg(rows).arg(cols));
return;
}
const int expected = rows * cols;
if (boardPacked.size() != expected) {
fail(QStringLiteral("보드 바이트 길이 오류"));
return;
}
QByteArray png;
{
QBuffer buf(&png);
buf.open(QIODevice::WriteOnly);
if (!boardImage.save(&buf, "PNG")) {
fail(QStringLiteral("PNG 인코딩 실패"));
return;
}
}
QByteArray body;
body.append(char(static_cast<unsigned char>(rows)));
body.append(char(static_cast<unsigned char>(cols)));
body.append(char(0));
packU32BE(&body, quint32(png.size()));
body.append(png);
body.append(boardPacked);
QByteArray frame;
frame.append("INV3", 4);
packU32BE(&frame, quint32(body.size()));
frame.append(body);
QString err;
bool ok = false;
for (int attempt = 0; attempt < 2 && !ok; ++attempt) {
if (m_cancel.loadAcquire() != 0) {
fail(QStringLiteral("사용자 취소"));
return;
}
if (!ensureConnected(host, port, qMin(15000, qMax(2000, timeoutMs / 8)), &err)) {
if (attempt == 1) {
fail(err);
return;
}
continue;
}
if (!writeAll(frame, qMin(60000, qMax(5000, timeoutMs / 4)))) {
err = m_socket ? m_socket->errorString() : QStringLiteral("송신 실패");
closeSocketLocked(QStringLiteral("write fail"));
continue;
}
ok = true;
}
if (!ok) {
fail(err);
return;
}
QByteArray head;
if (!readBytes(&head, 4, timeoutMs)) {
const bool canceled = m_cancel.loadAcquire() != 0;
closeSocketLocked(QStringLiteral("rply head fail"));
fail(canceled ? QStringLiteral("사용자 취소")
: QStringLiteral("응답 헤더 수신 실패 (타임아웃 가능)"));
return;
}
if (head != QByteArrayLiteral("RPLY")) {
closeSocketLocked(QStringLiteral("bad head"));
fail(QStringLiteral("잘못된 응답 헤더"));
return;
}
QByteArray flag;
if (!readBytes(&flag, 1, timeoutMs)) {
closeSocketLocked(QStringLiteral("flag fail"));
fail(QStringLiteral("응답 플래그 수신 실패"));
return;
}
if (static_cast<unsigned char>(flag.at(0)) == 1U) {
QByteArray rc;
if (!readBytes(&rc, 2, timeoutMs)) {
closeSocketLocked(QStringLiteral("rc fail"));
fail(QStringLiteral("액션 수신 실패"));
return;
}
const int action = int(static_cast<unsigned char>(rc.at(0)));
m_busy.storeRelease(0);
emit moveReady(true, action, 0, QString());
return;
}
QByteArray lenRaw;
if (!readBytes(&lenRaw, 2, timeoutMs)) {
closeSocketLocked(QStringLiteral("err len fail"));
fail(QStringLiteral("에러 길이 수신 실패"));
return;
}
QBuffer blen(&lenRaw);
blen.open(QIODevice::ReadOnly);
QDataStream dlen(&blen);
dlen.setByteOrder(QDataStream::BigEndian);
quint16 elen = 0;
dlen >> elen;
QByteArray emsg;
if (!readBytes(&emsg, elen, timeoutMs)) {
closeSocketLocked(QStringLiteral("err body fail"));
fail(QStringLiteral("에러 본문 수신 실패"));
return;
}
fail(QString::fromUtf8(emsg));
}
void Worker::requestLog(int rows, int cols, int action, int actor, int playMode, int status, int seq,
QByteArray boardPacked, QString host, quint16 port, int timeoutMs)
{
if (!validGrid(rows, cols))
return;
if (boardPacked.size() != rows * cols)
return;
QString err;
if (!ensureConnected(host, port, qMin(5000, qMax(1000, timeoutMs / 4)), &err))
return;
QByteArray body;
body.append(char(static_cast<unsigned char>(rows)));
body.append(char(static_cast<unsigned char>(cols)));
body.append(char(static_cast<unsigned char>(action & 0xff)));
body.append(char(static_cast<unsigned char>(actor & 0xff)));
body.append(char(static_cast<unsigned char>(playMode & 0xff)));
body.append(char(static_cast<unsigned char>(status & 0xff)));
const quint16 seq16 = static_cast<quint16>(qBound(0, seq, 65535));
body.append(char((seq16 >> 8) & 0xff));
body.append(char(seq16 & 0xff));
body.append(boardPacked);
QByteArray frame;
frame.append("INVL", 4);
packU32BE(&frame, quint32(body.size()));
frame.append(body);
if (!writeAll(frame, qMin(10000, qMax(2000, timeoutMs)))) {
closeSocketLocked(QStringLiteral("invl write fail"));
return;
}
QByteArray ack;
if (!readBytes(&ack, 4, qMin(10000, qMax(1000, timeoutMs)))
|| ack != QByteArrayLiteral("LACK")) {
closeSocketLocked(QStringLiteral("invl ack fail"));
}
}
} // namespace TcpVlmClient