アーキテクチャの概要
本稿では、Qtフレームワークを利用して、ローカルエリアネットワーク(LAN)内で動作するチャットアプリケーションを構築します。システムは主に2つの通信プロトコルに依存しています。
- UDPブロードキャスト: ユーザーのログイン・ログアウト状態の通知、およびテキストメッセージの送受信に使用します。コネクションレスの特性を活かし、複数クライアントへの同時配信を実現します。
- TCP通信: ファイル転送に使用します。C/S(クライアント/サーバー)モデルを採用し、信頼性の高いデータストリームを確保します。
1. メインウィンドウとコンタクトリストの設計
ユーザーインターフェースには、コンタクトリストを管理するためのドロワーと、個別のチャットウィンドウを実装します。ここでは、QToolBoxとQToolButtonを活用して、アコーディオン形式のリストを構築します。
// 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();
}
}