게시판오목 Next.js 웹과 VLM 착수 API 연결하기예제 소스

예제 소스

오목 Next.js 웹과 VLM 착수 API 연결하기 · 파일 59개

tcpvlmclient.cpp

#include "tcpvlmclient.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));
}

inline bool isValidBoardSize(int sz)
{
    return sz == 13 || sz == 15 || sz == 19;
}

} // namespace

// =================== Worker ===================

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()
{
    // 어느 스레드에서 호출되든 안전하도록 atomic flag 만 set.
    // 진행 중인 read/write 루프가 다음 폴링(≤200ms)에서 이 flag 를 보고 빠져나오며
    // 그 시점의 워커 코드가 closeSocketLocked() 로 깔끔히 정리합니다.
    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; // 이미 같은 곳으로 연결되어 있음.
    }

    // 다른 host/port 면 기존 연결 폐기.
    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;

        // QEventLoop::exec() 대기는 같은 워커 스레드에 큐된 다른 슬롯(requestLog 등)이
        // 재진입해 동일 소켓을 동시에 읽는 경쟁을 일으킬 수 있음 → m_socket 무효화 후
        // SIGSEGV(0 주소 읽기). waitForReadyRead 는 중첩 이벤트 루프 없이 블록한다.
        qint64 slice64 = remaining;
        if (slice64 > 200)
            slice64 = 200;
        if (slice64 < 50)
            slice64 = 50;
        const int slice = int(slice64);
        (void)m_socket->waitForReadyRead(slice);
    }
    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 boardSize, int aiSide,
                         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 (!isValidBoardSize(boardSize)) {
        fail(QStringLiteral("지원하지 않는 보드 크기: %1").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;
        }
    }

    // OMK3 body: u8 size, u8 ai_side, u32 png_len, PNG bytes, size*size board bytes.
    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);

    QByteArray frame;
    frame.append("OMK3", 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 r = int(static_cast<unsigned char>(rc.at(0)));
        const int c = int(static_cast<unsigned char>(rc.at(1)));
        m_busy.storeRelease(0);
        emit moveReady(true, r, c, 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 boardSize, int stone, int row, int col, int actor, int mode,
                        int status, int seq, QByteArray boardPacked, QString host, quint16 port,
                        int timeoutMs)
{
    // 로깅 전용 — 실패해도 사용자 흐름을 막지 않는다 (silently ignore).
    if (!isValidBoardSize(boardSize))
        return;
    const int expected = boardSize * boardSize;
    if (boardPacked.size() != expected)
        return;
    if (stone != 1 && stone != 2)
        return;

    QString err;
    if (!ensureConnected(host, port, qMin(5000, qMax(1000, timeoutMs / 4)), &err))
        return;

    QByteArray body;
    body.append(char(static_cast<unsigned char>(boardSize)));
    body.append(char(static_cast<unsigned char>(stone)));
    body.append(char(static_cast<unsigned char>(row & 0xff)));
    body.append(char(static_cast<unsigned char>(col & 0xff)));
    body.append(char(static_cast<unsigned char>(actor & 0xff)));
    body.append(char(static_cast<unsigned char>(mode & 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("OMKL", 4);
    packU32BE(&frame, quint32(body.size()));
    frame.append(body);

    if (!writeAll(frame, qMin(10000, qMax(2000, timeoutMs)))) {
        closeSocketLocked(QStringLiteral("omkl write fail"));
        return;
    }

    QByteArray ack;
    if (!readBytes(&ack, 4, qMin(10000, qMax(1000, timeoutMs)))
        || ack != QByteArrayLiteral("LACK")) {
        closeSocketLocked(QStringLiteral("omkl ack fail"));
        return;
    }
    // ACK 수신 OK. 시그널 안 쏘고 종료 (UI 영향 없음).
}

} // namespace TcpVlmClient