게시판SGF 기보로 학습하는 바둑 LLM 인공지능예제 소스

예제 소스

SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개

tcpvlmclient.cpp

#include "tcpvlmclient.h"

#include <QAbstractSocket>
#include <QBuffer>
#include <QDataStream>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QTcpSocket>
#include <QTimer>

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));
}

inline bool isValidBoardSize(int sz)
{
    return sz == 19; // 본 프로젝트는 19×19 만 지원
}

} // namespace

Worker::Worker(QObject *parent)
    : QObject(parent)
{
}

Worker::~Worker()
{
    closeAllSocketsLocked(QStringLiteral("worker destroyed"));
}

bool Worker::isBusy() const { return m_busy.loadAcquire() != 0; }
bool Worker::isConnected() const
{
    for (const auto &c : m_conns) {
        if (c.sock && c.sock->state() == QAbstractSocket::ConnectedState)
            return true;
    }
    return false;
}

void Worker::emitConnState()
{
    QString endpoint;
    bool connected = false;
    // UI 라벨은 "현재 연결 중인 엔드포인트가 있으면" 한 개만 보여도 충분.
    for (const auto &c : m_conns) {
        if (c.sock && c.sock->state() == QAbstractSocket::ConnectedState) {
            endpoint = QStringLiteral("%1:%2").arg(c.host).arg(c.port);
            connected = true;
            break;
        }
    }
    emit connectionStateChanged(connected, endpoint);
}

void Worker::closeAllSocketsLocked(const QString &reason)
{
    Q_UNUSED(reason);
    for (auto &c : m_conns) {
        if (c.sock) {
            c.sock->abort();
            c.sock->deleteLater();
            c.sock = nullptr;
        }
        c.host.clear();
        c.port = 0;
        c.lastUseSeq = 0;
    }
    emitConnState();
}

void Worker::cancel() { m_cancel.storeRelease(1); }
void Worker::closeConnection() { closeAllSocketsLocked(QStringLiteral("explicit close")); }

void Worker::shutdownForAppExit()
{
    m_cancel.storeRelease(1);
    closeAllSocketsLocked(QStringLiteral("app exit"));
    m_busy.storeRelease(0);
}

bool Worker::ensureConnected(QTcpSocket **outSock, const QString &host, quint16 port, int timeoutMs,
                             QString *errorOut)
{
    if (outSock)
        *outSock = nullptr;
    if (errorOut)
        errorOut->clear();
    // 1) 이미 같은 엔드포인트로 연결된 소켓이 있으면 재사용
    for (auto &c : m_conns) {
        if (c.sock && c.host == host && c.port == port
            && c.sock->state() == QAbstractSocket::ConnectedState) {
            c.lastUseSeq = ++m_useSeq;
            if (outSock)
                *outSock = c.sock;
            qDebug() << "[VLM] ensureConnected: REUSE" << host << port;
            return true;
        }
    }

    // 2) 빈 슬롯 또는 LRU 슬롯 선택 (최대 2개 유지)
    int slot = -1;
    for (int i = 0; i < 2; ++i) {
        if (!m_conns[i].sock) {
            slot = i;
            break;
        }
    }
    if (slot < 0) {
        slot = (m_conns[0].lastUseSeq <= m_conns[1].lastUseSeq) ? 0 : 1;
        qDebug() << "[VLM] ensureConnected: EVICT slot" << slot << "to connect" << host << port;
        if (m_conns[slot].sock) {
            m_conns[slot].sock->abort();
            m_conns[slot].sock->deleteLater();
            m_conns[slot].sock = nullptr;
        }
        m_conns[slot].host.clear();
        m_conns[slot].port = 0;
        m_conns[slot].lastUseSeq = 0;
    }

    qDebug() << "[VLM] ensureConnected: NEW slot" << slot << host << port;
    QTcpSocket *sock = new QTcpSocket();
    sock->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
    sock->setSocketOption(QAbstractSocket::LowDelayOption, 1);
    QObject::connect(sock, &QTcpSocket::disconnected, sock, [s = sock]() {
        qDebug() << "[VLM] socket DISCONNECTED (peer=" << s->peerName()
                 << ":" << s->peerPort() << " err=" << s->errorString() << ")";
    });
    QObject::connect(sock, QOverload<QAbstractSocket::SocketError>::of(
                              &QAbstractSocket::errorOccurred),
                     sock, [](QAbstractSocket::SocketError err) {
                         qDebug() << "[VLM] socket ERROR" << int(err);
                     });
    sock->connectToHost(host, port);
    const int connTo = qBound(500, timeoutMs, 15000);
    if (!sock->waitForConnected(connTo)) {
        if (errorOut)
            *errorOut = QStringLiteral("TCP 연결 실패: %1").arg(sock->errorString());
        sock->abort();
        sock->deleteLater();
        return false;
    }
    m_conns[slot].sock = sock;
    m_conns[slot].host = host;
    m_conns[slot].port = port;
    m_conns[slot].lastUseSeq = ++m_useSeq;
    emitConnState();
    if (outSock)
        *outSock = sock;
    return true;
}

bool Worker::writeAll(QTcpSocket *sock, const QByteArray &data, int timeoutMs)
{
    if (!sock || sock->state() != QAbstractSocket::ConnectedState)
        return false;
    sock->write(data);
    while (sock->bytesToWrite() > 0) {
        if (m_cancel.loadAcquire() != 0)
            return false;
        if (!sock->waitForBytesWritten(qMax(200, timeoutMs)))
            return false;
    }
    return true;
}

bool Worker::readBytes(QTcpSocket *sock, QByteArray *dst, qint64 nbytes, int deadlineMs)
{
    dst->clear();
    if (!sock)
        return false;
    QElapsedTimer deadline;
    deadline.start();
    while (dst->size() < nbytes) {
        if (m_cancel.loadAcquire() != 0)
            return false;
        if (sock->bytesAvailable() > 0) {
            QByteArray chunk = sock->read(nbytes - dst->size());
            if (!chunk.isEmpty())
                dst->append(chunk);
            if (dst->size() >= nbytes)
                return true;
            continue;
        }
        if (sock->state() != QAbstractSocket::ConnectedState)
            return false;
        const qint64 elapsed = deadline.elapsed();
        if (elapsed >= deadlineMs)
            return false;
        const qint64 remaining = deadlineMs - elapsed;

        QEventLoop loop;
        const auto cReady = QObject::connect(sock, &QTcpSocket::readyRead, &loop,
                                             &QEventLoop::quit);
        const auto cDisc = QObject::connect(sock, &QTcpSocket::disconnected, &loop,
                                            &QEventLoop::quit);
        const auto cErr = QObject::connect(
            sock, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::errorOccurred),
            &loop,
            [&loop](QAbstractSocket::SocketError) { loop.quit(); });
        QTimer poll;
        poll.setSingleShot(true);
        const int slice = int(std::min<qint64>(remaining, 200));
        poll.start(qMax(50, slice));
        QObject::connect(&poll, &QTimer::timeout, &loop, &QEventLoop::quit);
        loop.exec();
        QObject::disconnect(cReady);
        QObject::disconnect(cDisc);
        QObject::disconnect(cErr);
    }
    return true;
}

void Worker::doPing(QString host, quint16 port, int timeoutMs)
{
    m_cancel.storeRelease(0);
    QString err;
    QTcpSocket *sock = nullptr;
    if (!ensureConnected(&sock, host, port, timeoutMs, &err)) {
        emit pingReady(false, err);
        return;
    }
    if (!writeAll(sock, QByteArrayLiteral("PING"), timeoutMs)) {
        const QString e = sock ? sock->errorString() : QStringLiteral("PING 송신 실패");
        closeAllSocketsLocked(QStringLiteral("ping write fail"));
        emit pingReady(false, e);
        return;
    }
    QByteArray pong;
    if (!readBytes(sock, &pong, 4, timeoutMs) || pong != QByteArrayLiteral("PONG")) {
        closeAllSocketsLocked(QStringLiteral("pong recv fail"));
        emit pingReady(false, QStringLiteral("PONG 응답 없음/불일치"));
        return;
    }
    emit pingReady(true, QString());
}

void Worker::doPingProbe(QString tag, QString host, quint16 port, int timeoutMs)
{
    // 영구 소켓(m_socket) 을 절대 건드리지 않는 일회성 검사.
    // 인게임 진행 중에 다른 서버를 ping 해도 현재 연결이 끊기지 않게 한다.
    const QString endpoint = QStringLiteral("%1:%2").arg(host).arg(port);
    if (host.trimmed().isEmpty() || port == 0) {
        emit pingProbeReady(tag, false, endpoint, 0,
                            QStringLiteral("호스트/포트가 비어 있습니다"));
        return;
    }

    QElapsedTimer rtt;
    rtt.start();

    QTcpSocket sock;
    sock.connectToHost(host, port);
    const int connTo = qBound(500, timeoutMs, 30000);
    if (!sock.waitForConnected(connTo)) {
        emit pingProbeReady(tag, false, endpoint, int(rtt.elapsed()),
                            QStringLiteral("TCP 연결 실패: %1").arg(sock.errorString()));
        return;
    }

    sock.write(QByteArrayLiteral("PING"));
    while (sock.bytesToWrite() > 0) {
        const qint64 elapsed = rtt.elapsed();
        if (elapsed >= timeoutMs) {
            sock.abort();
            emit pingProbeReady(tag, false, endpoint, int(elapsed),
                                QStringLiteral("PING 송신 타임아웃"));
            return;
        }
        if (!sock.waitForBytesWritten(qMax(200, int(timeoutMs - elapsed)))) {
            sock.abort();
            emit pingProbeReady(tag, false, endpoint, int(rtt.elapsed()),
                                QStringLiteral("PING 송신 실패: %1").arg(sock.errorString()));
            return;
        }
    }

    QByteArray pong;
    while (pong.size() < 4) {
        const qint64 elapsed = rtt.elapsed();
        if (elapsed >= timeoutMs) {
            sock.abort();
            emit pingProbeReady(tag, false, endpoint, int(elapsed),
                                QStringLiteral("PONG 타임아웃 (%1ms)").arg(timeoutMs));
            return;
        }
        if (sock.bytesAvailable() > 0) {
            pong.append(sock.read(4 - pong.size()));
            continue;
        }
        const int slice = qMin<int>(int(timeoutMs - elapsed), 500);
        if (!sock.waitForReadyRead(qMax(50, slice))) {
            // 잠깐 못 받으면 다시 한 바퀴 (deadline 검사로 종료).
            // 단, 연결이 끊어졌더라도 OS 버퍼에 마지막 4바이트(PONG) 가
            // 남아 있을 수 있으므로 bytesAvailable() 을 한 번 더 확인.
            if (sock.bytesAvailable() > 0)
                continue;
            if (sock.state() != QAbstractSocket::ConnectedState) {
                sock.abort();
                emit pingProbeReady(tag, false, endpoint, int(rtt.elapsed()),
                                    QStringLiteral("연결 끊김 (PONG 미수신)"));
                return;
            }
        }
    }

    const int latency = int(rtt.elapsed());
    sock.disconnectFromHost();
    if (sock.state() != QAbstractSocket::UnconnectedState)
        sock.waitForDisconnected(200);

    if (pong != QByteArrayLiteral("PONG")) {
        emit pingProbeReady(tag, false, endpoint, latency,
                            QStringLiteral("응답 불일치 (%1)").arg(QString::fromLatin1(pong)));
        return;
    }
    emit pingProbeReady(tag, true, endpoint, latency, QString());
}

void Worker::requestMove(QImage boardImage, QByteArray boardPacked, int boardSize, int aiSide,
                         QString host, quint16 port, int timeoutMs, quint32 gameEpoch,
                         QByteArray rejectCoords)
{
    m_cancel.storeRelease(0);
    m_busy.storeRelease(1);

    auto fail = [&](const QString &msg) {
        m_busy.storeRelease(0);
        emit moveReady(false, -1, -1, false, msg);
    };

    if (!isValidBoardSize(boardSize)) {
        fail(QStringLiteral("지원하지 않는 보드 크기: %1 (19만 지원)").arg(boardSize));
        return;
    }
    const int expected = boardSize * boardSize;
    if (boardPacked.size() != expected) {
        fail(QStringLiteral("판 직렬화 크기 오류 (%1, 기대 %2)")
                 .arg(boardPacked.size())
                 .arg(expected));
        return;
    }
    if (aiSide != 1 && aiSide != 2) {
        fail(QStringLiteral("aiSide 값 오류"));
        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>(boardSize)));
    body.append(char(static_cast<unsigned char>(aiSide)));
    packU32BE(&body, quint32(png.size()));
    body.append(png);
    body.append(boardPacked);
    packU32BE(&body, gameEpoch);
    // 거부 좌표가 있을 때만 trailer 첨부 (구형 서버·빈 목록 호환).
    {
        int n = rejectCoords.size() / 2;
        if (n > 64) n = 64;
        if (n > 0) {
            body.append(char(static_cast<unsigned char>(n)));
            for (int i = 0; i < n; ++i) {
                body.append(rejectCoords.at(i * 2));
                body.append(rejectCoords.at(i * 2 + 1));
            }
        }
    }

    QByteArray frame;
    frame.append("GO19", 4);
    packU32BE(&frame, quint32(body.size()));
    frame.append(body);

    QString err;
    bool ok = false;
    QTcpSocket *sock = nullptr;
    for (int attempt = 0; attempt < 2 && !ok; ++attempt) {
        if (m_cancel.loadAcquire() != 0) {
            fail(QStringLiteral("사용자 취소"));
            return;
        }
        if (!ensureConnected(&sock, host, port, qMin(15000, qMax(2000, timeoutMs / 8)), &err)) {
            if (attempt == 1) {
                fail(err);
                return;
            }
            continue;
        }
        if (!writeAll(sock, frame, qMin(60000, qMax(5000, timeoutMs / 4)))) {
            err = sock ? sock->errorString() : QStringLiteral("송신 실패");
            continue;
        }
        ok = true;
    }
    if (!ok) {
        fail(err);
        return;
    }

    QByteArray head;
    if (!readBytes(sock, &head, 4, timeoutMs)) {
        const bool canceled = m_cancel.loadAcquire() != 0;
        fail(canceled ? QStringLiteral("사용자 취소")
                      : QStringLiteral("응답 헤더 수신 실패 (서버 추론 시간 초과 가능성)"));
        return;
    }
    if (head != QByteArrayLiteral("RPLY")) {
        fail(QStringLiteral("잘못된 응답 헤더"));
        return;
    }
    QByteArray flag;
    if (!readBytes(sock, &flag, 1, timeoutMs)) {
        fail(QStringLiteral("응답 플래그 수신 실패"));
        return;
    }
    if (static_cast<unsigned char>(flag.at(0)) == 1U) {
        QByteArray rc;
        if (!readBytes(sock, &rc, 2, timeoutMs)) {
            fail(QStringLiteral("좌표 수신 실패"));
            return;
        }
        const int r = int(static_cast<unsigned char>(rc.at(0)));
        const int c = int(static_cast<unsigned char>(rc.at(1)));
        const bool isPass = (r == 255 && c == 255);
        m_busy.storeRelease(0);
        emit moveReady(true, r, c, isPass, QString());
        return;
    }

    QByteArray lenRaw;
    if (!readBytes(sock, &lenRaw, 2, timeoutMs)) {
        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(sock, &emsg, elen, timeoutMs)) {
        fail(QStringLiteral("에러 본문 수신 실패"));
        return;
    }
    fail(QString::fromUtf8(emsg));
}

} // namespace TcpVlmClient