◆◆ ソケット通信 ◆◆
SFML2.5 非公式日本語翻訳
ソケット。
ソケットというのは、アプリケーションと外の世界の間にあるインターフェイスです。
ソケットを通して、データを送信したり受信したりできます。
なので、ネットワークを使うプログラムなら大体、ソケットを使うことになります。
ネットワークコミュニケーションの中心的な要素、という感じです。
ソケットには種類があって、それぞれ違った機能があります。
その中でも SFML は TCP と UDP というもっともポピュラーなソケットを実装してます。
TCP vs UDP
ソケットには TCPソケット と UDPソケットの2種類があります。
それぞれ「何ができて」「何ができないのか」を知っておきましょう。
そんでもって、キミが作りたいアプリケーションでは どっちを使えばいいのか、選ぶことにしましょう。
主な違いは、TCP では送受信を行う前に、相手マシンと接続しておく必要があるってことです(connection-based)。
一度接続した後は、接続した相手マシンとだけ、送受信できます。
なので、接続相手の数だけ別々に TCPソケットが必要、ということになります。
UDP は接続せずに使います(not connection-based)。1つのソケットで、いつでも誰とでも送受信できます。
2つ目の違い。
TCP には UDP と違って信頼性があります。
送信したデータが「壊れずに」「送った順番で」受信されること、が保証されてます。
UDP では、そういう確認をせずに送受信してるので、信頼性が保証されてません。
送ったデータがダブって受信されたり、送ったときと違う順番で受信されたりもします。
途中でデータが消失して相手マシンに届かないこともあります。でも届いたデータそのものは有効であることが保証されてます(壊れてないということ)。
こう聞くと UDP を使うのは、ちょっとおっかない感じがしますけど、"ほとんどの場合" データはちゃんと届きます。送った通りの順番で。
最後の違いは、データが送信される方式です。
TCP では ストリームプロトコルが使われてます。
メッセージとメッセージの間に境界線がありません。
たとえば "Hello" と送って、そのあと "SFML" と送ったとします。すると、送信先のマシンではどんなデータを受け取ることになるか?
"HelloSFML" かもしれないし "Hel" + "loSFML" かもしれないし、はたまた "He" + "loS" + "FML" なんてこともあるかもしれません。
UDP ではデータグラムプロトコルが使われてます。
データグラムというのは、データ同士が、お互いに混ざり合わないようになっているパケットです。
たとえば UDP で何かデータを受信したとすると、そのデータは、送信されたときと同じ状態であることが保証されてます。
あ、もう1つ違いがあった。
UDP は接続せずに通信するので、複数の受信相手にブロードキャストすることができます。
ネットワーク全体にブロードキャスト、なんてことも可能です。
1対1の通信を行う TCP では、こういうことはできません。
TCPソケットで接続
タイトルの通り、このパートは TCP についてです。
えー、TCPの接続というかコネクションには2つの接続口があります。
他のマシンからの接続を待ち構えている側(つまりサーバーですね)。
それと、自分からどこかに接続をする側(つまりクライアントですね)。
クライアント側では、話はカンタンです。
sf::TcpSocketクラスのインスタンスを作って、connect()関数で接続します。
#include <SFML/Network.hpp>
sf::TcpSocket socket;
sf::Socket::Status status = socket.connect("192.168.0.5", 53000);
if (status != sf::Socket::Done)
{
// error...
}
最初の引数は接続相手のアドレスです。
この引数の型は
sf::IpAddress です。
このクラスを使うと、URLでもIPアドレスでもホスト名でも、なんでも使えます。
詳しくは APIドキュメントを見てね。
2番目の引数は、接続先のポート番号です。
相手マシンがこのポート番号で接続を待ち受けていないと、接続は成功しません。
もう1つ、オプションとして3番目の引数もあります。タイムアウトの設定です。
タイムアウトが設定されている場合、設定されている時間が経過する前に接続が失敗すると、エラーが返ってきます。
設定していない場合は、OS のデフォルトのタイムアウト値が使われます。
接続できたら、getRemoteAddress()関数 と getRemotePort()関数で、相手マシンのアドレスやポート番号を取得することもできます(必要なら)。
デフォルトでは、ソケットクラスの関数はどれもブロッキングモードで実行されるようになっています。
どういうことかというと、1度実行されると、処理が完了するまでプログラム(またはその関数を実行したスレッド)は
停止した状態になる、ということです。
通信の処理は場合によっては時間がかかるので、その間プログラムは停止している必要があるのです。
たとえば、接続不可なマシンに接続しようとした場合は数秒待たないといけないし、
データを受信するときには、受信が完了してデータが使えるようになるまで待ってないといけない、などなど。
setBlocking()関数を使うと、この設定を変更して、処理をノンブロッキングモードで実行できるようになります。
詳しくはこのページの最後のパートを読んでくださいませ。
サーバー側では、クライアント側よりもすることがたくさんあります。
ソケット自体が複数必要です。クライアントからの接続を待ち受けるためのソケット(リスナー)。そして接続したクライアントごとに通信を行うソケットがそれぞれ必要です。
接続を待ち受けるには、
sf::TcpListenerクラスを使います。
これは接続を待ち受けるための専用のクラスです。データの送受信はしません。
sf::TcpListener listener;
// リスナーをポートにバインドする(クライアントからの接続を待ち受けるポートを指定)
if (listener.listen(53000) != sf::Socket::Done)
{
// error...
}
// 新たなコネクションを受け付ける
sf::TcpSocket client;
if (listener.accept(client) != sf::Socket::Done)
{
// error...
}
// 接続したクライアントとは "client" を使って通信します。
// その一方で、"listener" を使って次の接続を再び待ち受けることができます。
accept()関数を呼び出すと、次に誰かが接続してくるまで処理がそこでストップします(ブロッキングモードに設定されている場合)。
接続が来ると、引数に入れていたソケットが初期設定されて処理が戻ってきます。
このとき、引数に入れていたソケットを使って、接続してきたクライアントと通信できる状態になってます。
そして、リスナーは再び次の接続を待ち受けることができます。
connect()関数(クライアント側)と accept()関数(サーバー側)が成功すると、
コネクションが確立されて、両ソケットはお互いにデータをやりとりできるようになります。
UDPソケットを使う
UDPソケットはコネクションを確立せずに使うのですが、
データを受信するには、ポート番号を指定してバインドする必要があります。
コネクションを作る必要がないとは言っても、複数のポートにやってきたデータを同時に受信することはできないのですね。
sf::UdpSocket socket;
// ソケットを指定のポートにバインド
if (socket.bind(54000) != sf::Socket::Done)
{
// error...
}
ソケットをポートにバインドしたら、データを受信する準備完了です。
ちなみに、sf::Socket::AnyPort値 を使うと、空いてるポートを OS から自動的に取得できます。
実際にどのポートが取得されたのかは、socket.getLocalPort() で確認できます。
以上は UDPでデータを受信するやり方でした。
送信のやり方はと言うと、UDPソケットでデータを送信するために、特に前準備は必要ありません。
データを送って受信する
送受信の仕方は、どっちのタイプのソケットでも同じです。
1つだけ違うのは、UDP では引数が2つ多いということです。送信者/受信者のアドレスとポート番号です。
送信と受信のそれぞれに、2種類のやり方があります。
1つ目は、データを生のバイト列のままで送受信する「ローレベルな」方法。
もう1つは、生のデータではなく、
sf::Packetクラスでデータを送受信する「ハイレベル」なやり方です。このクラスについて詳しくは
パケットクラスのチュートリアルを見てね。
ここでは「ローレベルな」やり方についてだけ説明するよ。
データを送信するには send()関数を使います。
引数に、送信したいデータ(バイト列)へのポインタと、何バイト送りたいのかのバイト数を指定してね。
char data[100] = ...;
// TCP socket:
if (socket.send(data, 100) != sf::Socket::Done)
{
// error...
}
// UDP socket:
sf::IpAddress recipient = "192.168.0.5";
unsigned short port = 54000;
if (socket.send(data, 100, recipient, port) != sf::Socket::Done)
{
// error...
}
この send()関数に入れるポインタの型は void* です。なので、どんな型のアドレスでも入れれます。
でも、バイト列以外のデータを入れるのは多分バッドアイディアだよ。
サイズが1バイトより大きなネイティブ型は、マシンによってサイズが同じとは限らないんだ。
たとえば int や long はサイズが違っていて、エンディアンも違ってたりします。
そんなわけで、そういう型のデータは、違う OS 同士では正確に送受信できないかもしれない。
この件について詳しくは(解決策も)、
パケットクラスのチュートリアルでお話するね。
さて、UDPソケットを使うとネットワーク全体に同時にブロードキャストできます。
その場合、データの送信先アドレスには、sf::IpAddress::Broadcast っていう特別なアドレスを使うよ。
UPDを使うときには、もう1つ覚えておい欲しいことがあります。
データはデータグラムで送信されるんだったよね。そして、データグラムにはサイズの制限がある。
つまり、そのサイズを超えるデータを1度に送ることはできないのです。
毎回の send関数で送信できるデータの量は、sf::UdpSocket::MaxDatagramSize で定義されてて、
このバイト数を超えてはダメです。ちなみにこの値は 2^16 (65536) バイトより少し少ないぐらいです。
データを受信するには、 receive()関数を使います。
char data[100];
std::size_t received;
// TCP socket:
if (socket.receive(data, 100, received) != sf::Socket::Done)
{
// error...
}
std::cout << "Received " << received << " bytes" << std::endl;
// UDP socket:
sf::IpAddress sender;
unsigned short port;
if (socket.receive(data, 100, received, sender, port) != sf::Socket::Done)
{
// error...
}
std::cout << "Received " << received << " bytes from "
<< sender << " on port " << port << std::endl;
大事なことを1つ。
ソケットがブロッキングモードのとき、receive()関数は何かを受信するまで待機します。
つまりその間、この関数を呼んだスレッド(たぶんプログラム全体)が固まります。
さて、最初の引数は、バッファです。受信したデータがここにコピーされます。
2番目の引数が、そのデータの最大サイズです。
そして、実際に何バイトのデータを受信したのかが、3番目の引数に指定した変数に格納されます。
残り2つの引数は UDPソケットを使う場合のもので、送信者のアドレスとポートが入ってきます。
ここに入ってくる値を使って、レスポンスを返信することができるというわけです(必要なら)。
ここでお話したのは あくまでもデータを「ローレベルな」状態で送受信する方法です。
何か特別な事情がない限り、使わない方がいいです。
パケットを使うよりも強力で柔軟なプログラミングテクニックがない限り……。
ブロッキングモードで複数のソケットを同時に使う
通信相手が1人とは限らないわけで、1つのソケットでいちいちブロックがかかったてたら鬱陶しいですよね。
ソケット1がデータを受信して処理しようとしたところでソケット2がプログラムをブロックする、とか。
複数のソケットで同時に待ち受け処理をしたい。
たとえば待ち受けているソケットのうち、どれか1つがデータを受信するまで待機、みたいな。
ソケットセレクター(
sf::SocketSelectorクラス)を使うと、その夢が叶います。
ソケットには
sf::TcpSocket、
sf::UdpSocket、
sf::TcpListener、
といった種類があるわけですけど、
ソケットセレクターは、どの種類のソケットも監視できます。
add()関数を使って、セレクターにソケットを登録します。
sf::TcpSocket socket;
sf::SocketSelector selector;
selector.add(socket);
セレクターはソケットのリストというわけではありません。
登録されたソケットへの参照(ポインタ)を持っているだけです。ソケットそのものを保存しているわけではないです。
中に入れたソケットを取り出そうとしたり、いくつ入れたのかを直接カウントしようとはしないでね。
そういう処理がしたいときは それ用の別のクラスを使ってくださいませ(std::vector とか? std::list とか?)。
監視したいソケットを全部セレクターに入れたら、wait()関数を実行だ。
中に入れたソケットのうち、どれか1つがデータを受信する(か、エラーを出す)まで待機するよ。
タイムアウトを設定することもできます。設定した時間が経過しても何も受信しなかったら、関数が終了するよ。
この設定をしておくと、何も起きなくて永遠に固まったまま、という事態を回避できるというわけです。
if (selector.wait(sf::seconds(10)))
{
// received something
}
else
{
// タイムアウト。何も受信しませんでした…
}
wait()関数の戻り値が true だったとすると、
監視しているソケットのうちのどれか1つ(か1つ以上)のソケットがデータを受信した、ということです。
なので、receive()関数を実行しても、プログラムが固まる心配はありません。
ソケットの種類が
sf:TcpListener
の場合は、「データを受信した」じゃなく「接続が来た」という意味になりますね。
なのでその場合は receive()関数じゃなくて accept()関数ですね。すでに接続が来ているので、関数を実行してもブロックされる心配はありません。
さて、wait()関数が true だったとして、どのソケットがデータを受信したのでしょう?
セレクターはソケットのリストじゃないので、直接はわからないのです。
なので、isReady()関数を使って、容疑者を片っ端から調べます。
if (selector.wait(sf::seconds(10)))
{
// ループは好きなように書いてくださいませ。ソケットのリストをどう作っておくかはセレクターとは無関係です。
for ( ソケットのリストでループ )
{
if (selector.isReady(socket))
{
// 準備OKなソケットだ! receive()関数をブロックなしで実行できるぞ!(リスナーなら accept()だ!)
socket.receive(...);
}
}
}
APIドキュメントの
sf::SocketSelectorクラスのところに、
複数のクライアントからのコネクションやメッセージを扱うサンプルがあるので、見てみてくださいませ。
おまけ。
Selector::wait()関数のタイムアウトの機能を使うと、
タイムアウトつきの受信処理をカンタンに実装できます。ソケットクラス単独だとできませんが、やり方は簡単です。
sf::Socket::Status receiveWithTimeout
(sf::TcpSocket& socket, sf::Packet& packet, sf::Time timeout)
{
sf::SocketSelector selector;
selector.add(socket);
if (selector.wait(timeout))
{
return socket.receive(packet);
}
else
{
return sf::Socket::NotReady;
}
}
ノンブロッキングモード
デフォルトではどのソケットもブロッキングモードなのですが、
setBlocking()関数でいつでも設定を変更できます。
sf::TcpSocket tcpSocket;
tcpSocket.setBlocking(false);
sf::TcpListener listenerSocket;
listenerSocket.setBlocking(false);
sf::UdpSocket udpSocket;
udpSocket.setBlocking(false);
ノンブロッキングに設定すると、関数はすぐに結果を返してくるようになります。
たとえば receive()関数の場合だと、受信できるデータがない場合には 戻り値が sf::Socket::NotReady になります。
accept()関数の場合も、たまってる接続がなければ同じく 戻り値が sf::Socket::NotReady ですぐに終了します。
プログラムの中にメインループをすでに実装している場合は ノンブロッキングモードを使うのが早道です。
プログラムの流れを中断せずに、ソケットの状態を毎フレーム見ることができます。
sf::TcpSocket をノンブロッキングモードで使う場合、指定したデータが send()関数一回で全て送信されるとは限りません。
生データで送信する場合であっても、sf::Packet を使う場合であっても同じです。
SFML2.5 からは、ノンブロッキングモードの sf::TcpSocket で生データを送信する場合、
必ず send(const void* data, std::size_t size, std::size_t& sent) 関数を使って下さい。
実行後、実際に送信されたデータのバイト数が std::size_t& sent に入ってきます。
もしデータが部分的にしか送信されなかった場合の戻り値(リターンステータス)は sf::Socket::Partial です。
sf::Packet を使う場合でも生データを送信する場合でも同じです。
この値が返ってきたら、部分的にしか送信されなかったデータをキチンとお世話してあげないとデータの破損が発生してしまいます。
生データを送信しているならば、前回の送信が停止したバイトオフセットからデータを再送しましょう。
sf::Packet を使っているならば、バイトオフセットは sf::Packet オブジェクト自身の中に記録されてます。
なので、同じオブジェクトを使って、送信を繰り返して下さい。sf::Socket::Partial 以外の値が返ってくるまで、オブジェクトの中身をいじらずに送信を繰り返す。
sf::Packet オブジェクトを新たに作ってデータを入れ直して送る、なんてことをしてはダメです。
必ず前回使ったのと同一のオブジェクトで送信してください。