1. QTcpSocketの基本から実践までの正しい使い方
Qtのネットワークプログラミングを学び始める時、最初に出会うのがQTcpSocketです。公式ドキュメントや入門ガイドには、ソケットを作成し、サーバーに接続し、writeやreadを使用する方法が説明されています。しかし、プロジェクトの規模が大きくなると、特に大ファイルの送信、高負荷な同時接続、または高い安定性が求められる場合、単純な使い方は限界を迎えます。
QTcpSocketは、下層のBerkeley SocketAPIを上手くラッピングし、Qtの信号・スロットモデルに基づく非同期イベント駆動型を提供します。これにより、select、poll、epollなどの下層I/O多路復用機構を直接扱う必要がありません。しかし、このラッパーが優れているからといって、無条件に使用できるわけではありません。内部の仕組みを理解し、適切に使いこなす必要があります。
2. パケットの接着問題を解決:効率的なアプリケーション層プロトコルの設計
TCPはバイトストリーム型のプロトコルで、データの順序は保証されますが、メッセージ境界は維持されません。例えば、送信側が3回writeで「Hello」、「World」、「!」を送信しても、受信側は一度に「HelloWorld!」として受け取るか、あるいは「Hel」、「loWorld!」の2回に分けて受け取る可能性があります。
2.1 長さプレフィックス法
この方法は、ほとんどの場合で使用される最も信頼性の高い手法です。送信する各ペイロードの前に、固定長のフィールド(例えば4バイトのint)を送ります。このフィールドは、ペイロードの長さを示します。受信側は最初にこの長さフィールドを読み取り、その後に指定された長さのペイロードを読み取ります。
以下は、この手法を実践で使用した送信側のコード例です:
void sendMessage(QTcpSocket* socket, const QByteArray &payload) {
if (!socket || socket->state() != QAbstractSocket::ConnectedState) {
return;
}
// QDataStreamとQByteArrayを組み合わせて使用
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_5_15); // 版本を固定し、両端が一致していることを確認
// 初めに長さフィールドを0として書き込む
out << (quint32)0;
// 実際のペイロードを書き込む
out.writeRawData(payload.constData(), payload.size());
// バッファの先頭に戻り、正しいペイロード長を書き込む
out.device()->seek(0);
out << (quint32)(block.size() - sizeof(quint32)); // ペイロードの長さを設定
// 一度にソケットに書き込む
qint64 written = socket->write(block);
if (written != block.size()) {
// 書き込みが完了しなかった場合の処理
qWarning() << "データの書き込みが完了しませんでした。書き込んだバイト数:" << written << " / 必要なバイト数:" << block.size();
}
}
この例の重要な点は、最初に長さフィールドを0として書き込み、その後にペイロードを書き込んだのち、バッファの先頭に戻って実際の長さを書き込むことです。これにより、ペイロードの長さを事前に計算する必要がありません。
2.2 受信側の状態機械による完全なデータ受信
受信側はより注意が必要です。一度のreadyRead信号で完全なデータパケットを受信できるとは限らないからです。以下は、受信状態を管理するための状態機械の例です:
class MessageReceiver : public QObject {
Q_OBJECT
public:
explicit MessageReceiver(QTcpSocket* socket, QObject* parent = nullptr)
: QObject(parent), m_socket(socket), m_expectedSize(0) {
connect(m_socket, &QTcpSocket::readyRead, this, &MessageReceiver::onReadyRead);
}
private slots:
void onReadyRead() {
QDataStream in(m_socket);
in.setVersion(QDataStream::Qt_5_15); // 送信側と受信側のバージョンを一致させる
while (true) {
if (m_expectedSize == 0) {
// 状態:長さフィールドを待っている
if (m_socket->bytesAvailable() < sizeof(quint32)) {
return; // 受信データが足りないので、次回のreadyReadまで待つ
}
in >> m_expectedSize; // 長さフィールドを読み取る
}
// 状態:ペイロードを待っている
if (m_socket->bytesAvailable() < m_expectedSize) {
return; // 受信データが足りないので、次回のreadyReadまで待つ
}
// 足りるデータを受信し、処理する
QByteArray payload = m_socket->read(m_expectedSize);
emit messageReceived(payload); // 完成したペイロードを処理するためのシグナルを発信
// 次のメッセージの長さを待つ
m_expectedSize = 0;
}
}
private:
QTcpSocket* m_socket;
quint32 m_expectedSize;
};
このコードは、受信側が長さフィールドとペイロードを受信するための状態を管理します。一度のreadyReadでは完全なデータが受信できない場合、状態を維持し、次回の信号まで待機します。