Qtを用いたUDPブロードキャストとTCPファイル転送の実装:LANチャットアプリケーションの構築

アーキテクチャの概要

本稿では、Qtフレームワークを利用して、ローカルエリアネットワーク(LAN)内で動作するチャットアプリケーションを構築します。システムは主に2つの通信プロトコルに依存しています。

  • UDPブロードキャスト: ユーザーのログイン・ログアウト状態の通知、およびテキストメッセージの送受信に使用します。コネクションレスの特性を活かし、複数クライアントへの同時配信を実現します。
  • TCP通信: ファイル転送に使用します。C/S(クライアント/サーバー)モデルを採用し、信頼性の高いデータストリームを確保します。

1. メインウィンドウとコンタクトリストの設計

ユーザーインターフェースには、コンタクトリストを管理するためのドロワーと、個別のチャットウィンドウを実装します。ここでは、QToolBoxQToolButtonを活用して、アコーディオン形式のリストを構築します。

// MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QToolBox>
#include <QToolButton>
#include <QVBoxLayout>

class ChatWindow;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void openChatWindow();

private:
    QToolBox *contactDrawer;
    QToolButton *userButton;
    ChatWindow *activeChatWindow;
};

#endif // MAINWINDOW_H
// MainWindow.cpp
#include "MainWindow.h"
#include "ChatWindow.h"

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
    setWindowTitle(tr("LAN Chat Client"));
    resize(300, 500);

    contactDrawer = new QToolBox(this);
    setCentralWidget(contactDrawer);

    userButton = new QToolButton;
    userButton->setText(tr("User_A"));
    userButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    userButton->setAutoRaise(true);
    
    connect(userButton, &QToolButton::clicked, this, &MainWindow::openChatWindow);

    QWidget *drawerPage = new QWidget;
    QVBoxLayout *layout = new QVBoxLayout(drawerPage);
    layout->addWidget(userButton);
    layout->addStretch();
    
    contactDrawer->addItem(drawerPage, tr("Online Users"));
}

void MainWindow::openChatWindow()
{
    if (!activeChatWindow) {
        activeChatWindow = new ChatWindow(userButton->text());
    }
    activeChatWindow->show();
    activeChatWindow->raise();
    activeChatWindow->activateWindow();
}

MainWindow::~MainWindow()
{
    delete activeChatWindow;
}

2. UDPブロードキャストによるメッセージング

チャットメッセージやユーザーの在線状態をネットワーク内の全ノードに通知するため、QUdpSocketを用いたブロードキャスト通信を実装します。パケットには、メッセージタイプ(ログイン、ログアウト、テキスト)とペイロードを含めます。

// UdpMessenger.h
#ifndef UDPMESSAGER_H
#define UDPMESSAGER_H

#include <QObject>
#include <QUdpSocket>
#include <QHostAddress>

enum MessageType { Login, Logout, TextMsg };

class UdpMessenger : public QObject
{
    Q_OBJECT
public:
    explicit UdpMessenger(QObject *parent = nullptr);
    ~UdpMessenger();

    void broadcastMessage(MessageType type, const QString &content);
    
signals:
    void messageReceived(const QString &sender, const QString &message);
    void userStatusChanged(const QString &user, bool isOnline);

private slots:
    void processPendingDatagrams();

private:
    QUdpSocket *udpSocket;
    quint16 broadcastPort;
};

#endif // UDPMESSAGER_H
// UdpMessenger.cpp
#include "UdpMessenger.h"
#include <QByteArray>
#include <QDataStream>

UdpMessenger::UdpMessenger(QObject *parent) : QObject(parent)
{
    broadcastPort = 45000;
    udpSocket = new QUdpSocket(this);
    udpSocket->bind(broadcastPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
    connect(udpSocket, &QUdpSocket::readyRead, this, &UdpMessenger::processPendingDatagrams);
    
    // Notify network about login
    broadcastMessage(Login, "User_Online");
}

UdpMessenger::~UdpMessenger()
{
    broadcastMessage(Logout, "User_Offline");
}

void UdpMessenger::broadcastMessage(MessageType type, const QString &content)
{
    QByteArray datagram;
    QDataStream out(&datagram, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_12);
    out << static_cast<qint32>(type) << content;

    udpSocket->writeDatagram(datagram, QHostAddress::Broadcast, broadcastPort);
}

void UdpMessenger::processPendingDatagrams()
{
    while (udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(datagram.data(), datagram.size());

        QDataStream in(&datagram, QIODevice::ReadOnly);
        in.setVersion(QDataStream::Qt_5_12);
        
        qint32 type;
        QString content;
        in >> type >> content;

        switch (static_cast<MessageType>(type)) {
        case Login:
            emit userStatusChanged(content, true);
            break;
        case Logout:
            emit userStatusChanged(content, false);
            break;
        case TextMsg:
            emit messageReceived("Peer", content);
            break;
        }
    }
}

3. TCPを用いたファイル転送機能

ファイル転送はデータ欠落が許されないため、TCPプロトコルを使用します。送信側がQTcpServerとして待機し、受信側がQTcpSocketで接続を確立するC/Sモデルを構築します。転送効率を高めるため、ファイルを特定のチャンクサイズ(ペイロード)に分割してストリーム送信します。

3.1 ファイル送信側(Server)

送信側はファイルを選択し、TCPサーバーを起動して受信側の接続を待機します。接続確立後、ファイルサイズとファイル名をヘッダーとして送信し、続いてファイル本体をチャンク単位で転送します。

// FileSender.h
#ifndef FILESENDER_H
#define FILESENDER_H

#include <QDialog>
#include <QTcpServer>
#include <QTcpSocket>
#include <QFile>
#include <QElapsedTimer>

class FileSender : public QDialog
{
    Q_OBJECT
public:
    explicit FileSender(QWidget *parent = nullptr);
    ~FileSender();

    void startListening();
    void abortTransfer();

private slots:
    void handleNewConnection();
    void sendNextChunk();
    void updateProgress(qint64 numBytes);

private:
    QTcpServer *tcpServer;
    QTcpSocket *clientConnection;
    QFile *sourceFile;
    
    qint64 totalFileSize;
    qint64 bytesSent;
    qint64 remainingBytes;
    const qint64 chunkSize;
    
    QByteArray outBuffer;
    QElapsedTimer transferTimer;
    
    quint16 serverPort;
};

#endif // FILESENDER_H
// FileSender.cpp
#include "FileSender.h"
#include <QDataStream>
#include <QMessageBox>
#include <QFileInfo>

FileSender::FileSender(QWidget *parent) : QDialog(parent), chunkSize(64 * 1024)
{
    serverPort = 50000;
    tcpServer = new QTcpServer(this);
    connect(tcpServer, &QTcpServer::newConnection, this, &FileSender::handleNewConnection);
    
    totalFileSize = 0;
    bytesSent = 0;
    remainingBytes = 0;
    sourceFile = nullptr;
}

FileSender::~FileSender()
{
    abortTransfer();
}

void FileSender::startListening()
{
    if (!tcpServer->listen(QHostAddress::Any, serverPort)) {
        QMessageBox::critical(this, tr("Error"), tr("Failed to start TCP server."));
        return;
    }
}

void FileSender::handleNewConnection()
{
    clientConnection = tcpServer->nextPendingConnection();
    connect(clientConnection, &QTcpSocket::bytesWritten, this, &FileSender::updateProgress);
    connect(clientConnection, &QTcpSocket::disconnected, clientConnection, &QTcpSocket::deleteLater);

    transferTimer.start();
    sendNextChunk();
}

void FileSender::sendNextChunk()
{
    outBuffer.clear();
    QDataStream out(&outBuffer, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_12);

    if (bytesSent == 0) {
        QString fileName = QFileInfo(sourceFile->fileName()).fileName();
        out << totalFileSize << fileName;
    }

    qint64 bytesToRead = qMin(remainingBytes, chunkSize);
    out.writeRawData(sourceFile->read(bytesToRead).constData(), bytesToRead);
    
    clientConnection->write(outBuffer);
}

void FileSender::updateProgress(qint64 numBytes)
{
    bytesSent += numBytes;
    remainingBytes = totalFileSize - bytesSent;

    if (remainingBytes > 0) {
        sendNextChunk();
    } else {
        sourceFile->close();
        tcpServer->close();
    }
}

void FileSender::abortTransfer()
{
    if (tcpServer->isListening()) tcpServer->close();
    if (clientConnection) clientConnection->abort();
    if (sourceFile && sourceFile->isOpen()) sourceFile->close();
}

3.2 ファイル受信側(Client)

受信側はUDP経由でファイル転送要求を受け取った後、TCPクライアントとして送信側に接続します。ストリームからヘッダー情報を読み取り、ファイルを作成してデータを書き込みます。

// FileReceiver.h
#ifndef FILERECEIVER_H
#define FILERECEIVER_H

#include <QDialog>
#include <QTcpSocket>
#include <QFile>
#include <QElapsedTimer>

class FileReceiver : public QDialog
{
    Q_OBJECT
public:
    explicit FileReceiver(QWidget *parent = nullptr);
    ~FileReceiver();

    void connectToSender(const QString &ipAddress);
    void cancelReception();

private slots:
    void readIncomingData();

private:
    QTcpSocket *tcpClient;
    QFile *destinationFile;
    
    qint64 totalFileSize;
    qint64 bytesReceived;
    QString incomingFileName;
    
    QByteArray inBuffer;
    QElapsedTimer transferTimer;
    
    quint16 targetPort;
};

#endif // FILERECEIVER_H
// FileReceiver.cpp
#include "FileReceiver.h"
#include <QDataStream>
#include <QMessageBox>

FileReceiver::FileReceiver(QWidget *parent)
{
    targetPort = 50000;
    tcpClient = new QTcpSocket(this);
    connect(tcpClient, &QTcpSocket::readyRead, this, &FileReceiver::readIncomingData);
    
    totalFileSize = 0;
    bytesReceived = 0;
    destinationFile = nullptr;
}

FileReceiver::~FileReceiver()
{
    cancelReception();
}

void FileReceiver::connectToSender(const QString &ipAddress)
{
    bytesReceived = 0;
    tcpClient->abort();
    tcpClient->connectToHost(ipAddress, targetPort);
    transferTimer.start();
}

void FileReceiver::readIncomingData()
{
    QDataStream in(tcpClient);
    in.setVersion(QDataStream::Qt_5_12);

    if (totalFileSize == 0) {
        if (tcpClient->bytesAvailable() < sizeof(qint64)) return;
        in >> totalFileSize;
    }
    
    if (destinationFile == nullptr) {
        in >> incomingFileName;
        destinationFile = new QFile(incomingFileName);
        if (!destinationFile->open(QFile::WriteOnly)) {
            cancelReception();
            return;
        }
    }

    if (destinationFile->isOpen()) {
        inBuffer = tcpClient->readAll();
        destinationFile->write(inBuffer);
        bytesReceived += inBuffer.size();
        inBuffer.clear();
    }

    if (bytesReceived >= totalFileSize) {
        destinationFile->close();
        tcpClient->close();
    }
}

void FileReceiver::cancelReception()
{
    tcpClient->abort();
    if (destinationFile && destinationFile->isOpen()) {
        destinationFile->close();
        destinationFile->remove();
    }
}

タグ: Qt C++ udp TCP NetworkProgramming

8月14日 00:07 投稿