◆◆ ソケット通信 ◆◆
非公式日本語翻訳
ソケット。
ソケットというのは、アプリケーションと外の世界の間にある「門」みたいなものです。
ソケットを通して、データを送ったり受信したりできます。
なので、ネットワークを使うプログラムなら大体、ソケットを使うことになります。
ネットワークコミュニケーションの中心的な要素、という感じです。
TCP vs UDP
ソケットには TCPソケット と UDPソケットの2種類があります。
それぞれ「何ができて」「何ができないのか」を知っておきましょう。
そんでもって、キミが作りたいアプリケーションでは どっちを使えばいいのか、選ぶことにしましょう。
主な違いは、TCP では送受信を行う前に、相手マシンと接続しておく必要があるってことです。
一度接続した後は、接続した相手マシンとだけ、送受信できます。
なので、接続相手の分だけ別々の TCPソケットが必要、ということになります。
UDP は接続せずに使います。1つのソケットで、いつでも誰とでも送受信できます。
2つ目の違い。
TCP の方が UDP よりも信頼性が上です。
送信したデータが「壊れずに」「送った順番で」受信されること、が保証されてます。
UDP では、そういう確認をせずに送受信してるので、そこまでキッチリしてません。
送ったデータがダブって受信されたり、送ったときと違う順番で受信されたりもします。
途中でデータが消失して相手マシンに届かないこともあります。でも届いたデータそのものは有効です(壊れてない)。
こう聞くと UDP は、ちょっとおっかない感じがしますけど、"ほとんどの場合" データはちゃんと届きます。送った通りの順番で。
3つ目の違い。
これは 2つ目の違いの結果なんですけど、
UDP の方が TCP よりも早くて軽量です。してることが少ないので、オーバーヘッドも少なめです。
最後の違いは、データが送信される方式です。
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::TcpListenreクラスを使います。
接続を待ち受けるための専用のクラスです。データの送受信はしません。
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))
{
// this socket is ready, you can receive (or accept if it's a listener)
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 ですぐに終了します。
プログラムの中にメインループをすでに実装している場合は ノンブロッキングモードを使うのが早道です。
プログラムの流れを中断せずに、ソケットの状態を毎フレーム見ることができます。