◆◆ パケットクラスの使い方+拡張方法 ◆◆
SFML2.3 非公式日本語翻訳
問題発生? なになに?
ネットワーク越しにデータをやりとりするってのはですね、実はですね、けっこう、ややこしいものなのです。
なにしろ、違うマシンやら 違う OS やら 違う CPU やらが お話するわけです。
なので、そういう違うマシン同士で、会話をかみ合わせようとすると、いろいろと問題が起きてしまうのですね。
まず「エンディアン」
エンディアンというのは、データの並び順です。
1バイト以上のプリミティブ型のデータ(整数や浮動小数点)を、CPUがどういう順番で解釈するか?
このエンディアンには主に2種類あります。1つはビッグエンディアン。
ビッグエンディアン方式を採用している処理系では、最上位のバイトから順番にデータが記録されています。
もう1つはリトルエンディアン。こちらは最下位のバイトからデータが記録されます。
他にも、世間にはエキゾチックなバイトオーダーがあったりするんですが、出くわすことは滅多にないと思います。
さて、そんなわけで、何が問題なのかはわかりますよね?
エンディアンが異なっているマシンの間でデータを送信すると、同じ値になってくれません。
たとえば、16ビット整数値の "42" はビッグエンディアンでは 00000000 00101010 になります。
でもこれを リトルエンディアンのマシンに送信すると、"10752" と解釈されてしまいます。
トラブルその2。
プリミティブ型のサイズ。
C++の仕様では、プリミティブ型のサイズが規定されてません(char、short、int、long、float、double)。
なので、やっぱり、異なる処理系の間ではサイズも異なってる可能性があるのですね。
たとえば long int型は、マシンによって 32ビットだったり64ビットだったりします。
トラブルその3は TCP の仕組みに特有のものです。
TCP ではメッセージの境界線が保存されてなくて、送信したデータが勝手に分割されたりくっつけられたりするので、
受信する人はメッセージを読む前に、元の形に正しく復元してあげないと、おかしなことになってしまいます。
不完全なデータを読み込んでしまうとか。必要な部分を見逃してしまうとか。
ネットワークプログラミングをしていると、他にも、あれこれと問題が起きてきて、
なかなか避けては通れないんですけど、そういうのは、コンピューターの裏側の仕組みに由来している類の面倒な話ですよね。
そんなわけで、SFMLはその手のトラブルを避けて通るためのシンプルなツールを提供してます。
プリミティブ型のサイズを固定しちゃいました。
プリミティブ型をネットワークを通して通信すると、おかしなことになってしまうのでしたね。
じゃあ、どうしましょう? そうだ、使わなきゃいい!
というわけで、SFML は 通信用にサイズを固定したデータ型を用意しました
sf::Int8, sf::Uint16, sf::Int32、などなど。
この子たちは単にプリミティブ型を typedef したものなんですけど、プラットフォームの仕様に合わせて、
期待通りのサイズになるようになってます。
これを使うと異なるマシンの間で安全にデータを送受信することができます(ていうか、できないと困ります!)。
SFML が提供しているのは、固定サイズの整数型だけです。
浮動小数点型には、もともと、対応する固定サイズの型が別にあるはずです。
ていうか、普通はそこまで気にする必要ないと思います(少なくとも SFMLが動いてるマシンでは)。
float や double は必ず同じサイズです(それぞれ 32ビットと64ビット)
パケットクラス
残り2つの問題(エンディアンと、メッセージの境界線が保存されない件)は
sf::Packetクラスにデータを詰め込むと解決できます。
さらにボーナスとして素敵なインターフェイスもついてるぞ。古き良き日のバイト配列とは おさらばだ。
Packetクラスには普通のストリームみたいなインターフェイスがあって、
<<演算子でデータを挿入、>>演算子でデータを取得できます。
// 送信側です。
sf::Uint16 x = 10;
std::string s = "hello";
double d = 0.6;
sf::Packet packet;
packet << x << s << d;
// 受信側です。
sf::Uint16 x;
std::string s;
double d;
packet >> x >> s >> d;
書き込むときは気にしなくていいんですけど、
パケットから読み込むときは、中に入ってる以上のバイト数を読み込もうとすると失敗してしまいます。
読み込みが失敗すると、パケットクラスにエラーのフラグが設定されます。
エラーになっているかどうかは、ブール値と同じような感じで調べることができます(普通のストリームのときと同じ要領)。
if (packet >> x)
{
// ok
}
else
{
// error, 'x' を読み込めなかった
}
パケットクラスでの送受信は、バイト配列の送受信と同じくらいカンタンです。
SFML のソケットクラスで
sf::Packet のオブジェクトをそのまま送受信できます。
// TCPソケットを使う場合
tcpSocket.send(packet);
tcpSocket.receive(packet);
// UDPソケットを使う場合
udpSocket.send(packet, recipientAddress, recipientPort);
udpSocket.receive(packet, senderAddress, senderPort);
パケットクラスを使うことで "メッセージの境界線" の問題も解決できます。
どういうことかというと、TCPソケットでパケットクラスのデータを送信した場合、
全く同じ内容のパケットが受信側に届きます。
送ったデータの一部が欠けることもないし、次に送ったデータと混ざることもありません。
その代わり欠点もあって、sf::Packetクラスは メッセージの境界線の情報を保存しているので、
データそのものよりも多めのバイト数を送信することになります。
それと、sf::Packetクラスでデータを送ったら、受信する側でもsf::Packetとしてデータを受け取る必要があります。
じゃないと、データを復元できません。
要するに、SFML のパケットクラスのデータを、SFML を使ってないクライアントに送ることはできません。
ただし、これは TCP の場合だけの話です。 UDP はもともとメッセージの境界線問題がないので、大丈夫です。
ユーザー定義型と一緒に使えるように拡張する
パケットクラスの演算子はオーバーロードされてて、全てのプリミティブ型と、その他の主な型の計算ができるようになってます。
でも、あなたの独自の型を計算するにはどうしましょう?
標準のストリームと同じように <<演算子と >> 演算子をオーバーロードすることで
あなたが作った型にも、sf::Packet との互換性を持たせることができます。
struct Character
{
sf::Uint8 age;
std::string name;
float weight;
};
sf::Packet& operator <<(sf::Packet& packet, const Character& character)
{
return packet << character.age << character.name << character.height;
}
sf::Packet& operator >>(sf::Packet& packet, Character& character)
{
return packet >> character.age >> character.name >> character.height;
}
どちらの演算子も sf::Packet への参照を返しているので、1つの計算式の中で何度も使えます。
さて、演算子を定義したので、sf::Packet に Character のデータを挿入したり取得したりできます。
他のプリミティブ型を使うときと同じ感じですね。
Character bob;
packet << bob;
packet >> bob;
パケットクラスをカスタマイズする
パケットクラスを使うと、生のデータを使うよりも便利なのでした。
ここでさらに、独自の機能を追加する方法についてお教えいたしましょう。
たとえば、自動的にデータを圧縮するとか、暗号化するとか。
やり方はカンタンです。
sf::Packetクラスを継承して、下記の関数をオーバーライドします。
- onSend()関数 : ソケットがデータを送信する前に呼ばれます
- onReceive()関数 : ソケットがデータを受信した直後に呼ばれます
上記の関数の中で、データに直接アクセスできます。
後はもう、好きなようにデータを加工してください。
以下は擬似コードですが、パケットを自動的に圧縮/展開するサンプルです。
class ZipPacket : public sf::Packet
{
virtual const void* onSend(std::size_t& size)
{
const void* srcData = getData();
std::size_t srcSize = getDataSize();
return compressTheData(srcData, srcSize, &size); // もちろん擬似関数だよ!
}
virtual void onReceive(const void* data, std::size_t size)
{
std::size_t dstSize;
const void* dstData = uncompressTheData(data, size, &dstSize); // 擬似関数ですよー。
append(dstData, dstSize);
}
};
こういうふうにして作ったクラスも、
sf::Packetクラスと全く同じように使えます。
演算子のオーバーロードも有効なままです。
ZipPacket packet;
packet << x << bob;
socket.send(packet);