SI Electronics Ltd.
これは、Be, Inc. 社のProgramming Tutorial: Networking を翻訳したものです。誤訳など、不備の点がありましたら、訳者の エスアイエレクトロニクスまでご指摘いただければ幸いです。
なお、ソケットによるネットワーキングの基本を学びたい方は、雪田修一著「UNIX ネットワーク・ベストプログラミング入門」(技術評論社)もお読みになることをお勧めします。このチュートリアルの内容を理解する上で最良の参考書です。
プログラミング チュートリアル:ネットワーキング
[注意: このチュートリアルは読者が BeOS プログラミングの基本を理解していることを仮定しています。 他のチュートリアル, 特に Approaching Be は、プログラム初心者の助けになります(訳注: 長田正彦氏による日本語訳があります)。 The Be Book が手近にあると役に立つでしょう。このチュートリアルのソースコードは、 ftp://ftp.be.com/pub/Samples/net_tutorial.tgz に置いてあります。]
このチュートリアルの目的は、実際に動く TCP/IP クライアント・サーバーの簡単な例題のステップを追ってみることです。例題のアプリケーションは、複数のユーザーがサーバーに接続し、共有するウィンドウに描画できる「仮想黒板」です。ユーザーの一人がウィンドウに描画した画像は他のすべてのユーザーのウィンドウに現れます。その他にもう一つできることがあり、それはアプリケーション・メニューにある「clear」コマンドで、これですべての黒板を消します。このチュートリアルでは、プログラムのユーザー・インターフェースの部分は大部分無視し、ネットワークの部分に重点を置くことにします。
最初にお断りして置きますが、 DR8 のネットワーキングにはいくつか問題があります。詳しいことはこのチュートリアル末尾の "その他の注意事項" を見て下さい。しかしここで使用した socket code は、DR9 でも変更無しでコンパイルできるはずです。(すくなくとも作成中の DR9 の現在のバージョンでは OK です。)
スレッドや BeOS の BLooper と BMessage オブジェクトについて、よく知らない方は、これらのセクションをまず読んで下さい。
ネットワークにつながっていなくても、心配御無用です。BeOS に組み込まれているループバックアドレス 127.0.0.1 をあなたのアドレスとして使用することができ、すべてうまく動きます。
チュートリアルのすべてのネットワーキングの基礎に二つのクラス、BTSSocket
と BTSAddress
があります。 BTSSocket
オブジェクトは、クライアント、サーバー双方のソケット接続を表わします。BTSAddress
オブジェクトはネットワーク・アドレスを表わします。
クライアント、サーバーのどちらも、2層から構成されています。一つの層は、一般的な
BMessage
を基礎とするクライアントとサーバーです。サーバーはクライアントからデータを受け取ると作動し、データを
BMessage
オブジェクトに変換し、その結果できたメッセージをアサインされているレシーバにポストします。メッセージをクライアントに送るには、BMessage
は、このサーバー・オブジェクトにポストされます。メッセージは特定のクライアントに宛てることもでき、すべてのクライアントに同時に送ることもできます。同様にメッセージのクライアントはサーバーから受け取ったデータをBMessage
に変換し、アサインされているメッセージレシーバにポストします。クライアントに局所的にポストされたメッセージはすべてサーバーに送られます。
これらの上にドローイング・サーバーとドローイング・クライアントのコードがあります。それぞれがBMessage
サーバーかクライアントのインスタンスを作成し、自分自身をメッセージ受取人と指定することで作動します。
このプログラムで使用される一般的なクラスの要約を記します。
BTSAddress.cpp -- ネットワーク・アドレス・オブジェクトの定義。 BTSSocket.cpp -- ネットワーク・ソケット・オブジェクトの定義。下位のネットワーク 関数(bind, send, recv 等) を実装します。 BTSNetMsgUtils.cpp -- BMessage をネットワークを通じて送ったり、受け取ったりするのに、 クライアントとサーバーの両方で使われるユーティリティ関数です。 BTSNetMsgServer.cpp -- BMessage を基礎とするサーバーの定義。 BTSServerMonitor.cpp -- 簡単なサーバー・ユーザー・インターフェースの表現。サーバーを 起動し、いくつかの接続を表示することができます。またループバック を選択することができます。 BTSNetMsgClient.cpp -- BMessage を基礎とするクライアントの定義。 BTSPrefDlog.cp -- 一般的なクライアント・インターフェース。どのホストに接続する かを指定できます。
その他に、ドロー・サーバーとクライアントを作成するために次のクラスがあります。
BTSNetDrawServer.cpp -- BTSNetMsgServer のインスタンスを作成し、それを管理します。 BTSNetDrawClient.cpp -- BTSNetMsgClient のインスタンスを作成し、それを管理します。 BTSNetDrawWindow.cpp -- クライアントの描画用ウィンドウです。 BTSNetDrawView.cpp -- クライアントの描画用ビューです。
最初に一般的なメッセージサーバーについて、調べましょう。
サーバーは、BLooper
です。これはあなたがそれにメッセージをポストすることで、通信できることを意味します。スレッドは(accept()
のような)ネットワーク関数においてブロッキングを避けるのに必要なときのみに使用されます。すべてのクライアント入力データは、ただ一つのスレッドで管理されます。出力データは、メッセージ・サーバーの
MessageReceived()
メソッドにより、直列化されます。メッセージ・サーバーには、全部で次の三つのスレッドがあります。
BLooper
の下位クラスとして作成されたサーバー。これがメッセージをクライアントに送りたい時にポストする宛先のスレッドです。
ドローイング・サーバーは、また他のアプリケーションと同様にBApplication
と BWindow
スレッドを作成します。
サーバーは、ある特定のポート番号を指定し、自分をそれに結び付けます。そしてクライアントが接続するのを待ちます。クライアントは、サーバーに接続できるためには、前以ってそのポート番号を知らなければなりません(あるいはサービス名でもよい。これはポート番号のエイリアス-別名-です)。
サーバーが正式に機能するまでにいくつかの関門を通過しなければなりません。このプロセスは
BTSNetMsgServer::Run()
メソッドの中にカプセル化されています。サーバーは、あるポートに結び付けられる前に、次の情報を必要とします。
port -- クライアントとサーバーの待ち合わせ点として使われる番号。 address -- サーバーが使用するネットワーク・インターフェースのアドレス(複数個の インターフェースがあってもよい)。DR8 では、同時に複数個のインターフ ェースを結び付ける上で問題があります。くわしくは、 The Be Book を見て下さい。 family -- ソケット・ネットワーク・アドレス・フォーマット。現在の所、AF_INET でなければならない。 type -- SOCK_STREAM と SOCK_DGRAM のどちらか。ストリームとデータグラム・ ソケットに対応。(TCP/IP はストリーム・ソケットです). protocol -- ソケットの通信プロトコル。もし 0 に設定すると、ソケット・タイプに 標準的なプロトコルが選ばれる。 SOCK_STREAM, の場合は、IPPROTO_TCP (TCP プロトコル)となる。
すなわち、「私は、これこれのネットワーク・インターフェースを使用して、これこれのポートに接続し、これこれの通信規約に従って通信したい。」と指定することになります。このアプリでは、通信は、常に TCP/IP で行います。
下位レベルで、この情報を使って接続するのに必要なステップは、
socket()
関数を使ってソケットを作成する。
bind()
関数を使ってソケットをアドレスに結び付ける。
listen()
関数を呼び、クライアント待機を開始する。
これらの事を具体的にどのようにするかは、 BTSSocket
クラスの中に隠されています。それらは、コンストラクタと Bind()
、Listen()
両メソッドの中にあります。くわしく知りたい人は、コードをチェックし、
Network
Kit の議論を読んで下さい。
サーバーがクライアント接続を受け入れるのに使用される BTSSocket
オブジェクトは、 BTSNetMsgServer
のコンストラクタにより生成されます。
BTSNetMsgServer::BTSNetMsgServer(const unsigned short port, BLooper* messageReceiver, const long priority, const int maxConnections, const unsigned long address, const int family, const int type, const int protocol ) : fSocket(type,protocol, family), fAddress(family,port, address), fPriority(priority), fMaxConnections(maxConnections) { if (messageReceiver == NULL) { messageReceiver = be_app; } fIsExiting = FALSE; fSocketListSem = ::create_sem(1, "Client Socket List Sem"); ::acquire_sem(fSocketListSem); fMessageReceiver = messageReceiver; return; }
BTSAddress
オブジェクトも作られますが、これは family
、
port
、 hostname
をカプセル化し、アドレスに関連するホスト名を与えるなどの変換を行う便宜的なクラスです。作られたソケットはサーバーがクライアントの接続を待つために使用するソケットです。
ソケットが作られたら、これをバインドし、クライアントからの接続待機を始めなければなりません。これは、サーバーのスレッドが
Run()
メソッドにより起動されることにより始められます。
thread_id BTSNetMsgServer::Run() { thread_id theID = -1; // To return thread id of this thread int result = 0; // Results of socket function calls // Bind the socket to the port/address specified in sockAddr result = fSocket.BindTo(fAddress); if (result >= 0) // Was bind successful? { // Start listening for connections. result = fSocket.Listen(fMaxConnections);
サーバーが、有効なソケットを持ったならば、バインドにより、それをネットワーク・アドレスに結び付けます(クライアントはこのアドレスと通信することになります)。それからクライアント待機状態に入るために
Listen()
が呼ばれ、サーバーがハンドルできる最大の接続数が
Listen()
に渡されます。
残りのステップは、クライアントを受け付け、クライアントのデータを受け取ることです。しかし、もし接続するクライアントがなかったり、クライアントがだれもサーバーにデータを送って来なかったならば、クライアントを受け付けたり、データを受け取ったりする試みは、無限に待ち続け、プログラムをブロックすることになります。これを避けるため、サーバーはこれらのおのおのにスレッドを産みます。
BTSNetMsgServer::Run()
を続けます。
if (result >= 0) { //Start the main server thread. theID = BLooper::Run(); if (theID > B_NO_ERROR) // Did server thread start? { // Start separate thread to handle conn. requests. fConnectionRequestHandlerID = ::spawn_thread(HandleConnectionRequests, kConnectionRequestHandlerName, fPriority, (void*)this); if (fConnectionRequestHandlerID > B_NO_ERROR) { ::resume_thread(fConnectionRequestHandlerID); } // Start separate thread to listen for incoming client data. fClientListenerID = ::spawn_thread(ListenToClients, kClientListenerName, fPriority, (void*)this); if (fClientListenerID > B_NO_ERROR) { ::resume_thread(fClientListenerID); } } }
Run()
は、これで終わりです。次に HandleConnectionRequests()
を見ましょう。これは、BTSNetMsgServer
クラスの静的で私的なメソッドです。
long BTSNetMsgServer::HandleConnectionRequests(void* arg) { BTSNetMsgServer* thisServer = (BTSNetMsgServer*)arg; BTSSocket acceptSocket = thisServer->Socket(); int clientSocket; sockaddr_in clientInterface; int clientIntfSize = sizeof(sockaddr_in); long result = 0; PRINT( ("HandleConnectionRequests - THREAD ENTER¥n")); // Thread blocks here, on accept(). while ((clientSocket = ::accept(acceptSocket.ID(), (struct sockaddr*)&clientInterface, &clientIntfSize)) >= 0) { PRINT(( "Connection request, new socket is %d¥n", clientSocket)); // A client has requested a connection. Make a handler for the // client socket. if (clientSocket >= 0) { BTSSocket* newClient = new BTSSocket(clientSocket); // Tell the server about the new client. BMessage* aMessage = new BMessage(NEW_CLIENT_MSG); if (aMessage != NULL) { if (aMessage->Error() == B_NO_ERROR) { aMessage->AddObject(SOURCE_SOCKET, (BObject*)newClient); if (aMessage->Error() == B_NO_ERROR) { thisServer->PostMessage(aMessage); } else delete aMessage; } else delete aMessage; } } else break; clientIntfSize = sizeof(sockaddr_in); } PRINT( ("HandleConnectionRequests - THREAD EXIT¥n")); exit_thread(result); }
始めに述べたように、このスレッドは accept()
関数により、いつもブロックされ、クライアントが接続を要求したときにだけリターンします。リターンしたときには、クライアントの局所的なソケット番号が得られます。(クライアント・マシンのソケット番号は同じでないかも知れません)。この時は、新しいBTSSocket
オブジェクトを作り、それをBMessage
に置き、サーバーにメッセージをポストします。サーバーはソケット・オブジェクトのリストを保管します。
クライアントが接続されたので、サーバーはクライアントからデータを受け取ることができます。それはサーバーのListenToClients()
メソッドが走っているスレッド内に到着することになります。ListenToClients()
の主なループを見てみましょう。
long BTSNetMsgServer::ListenToClients(void* arg) { BTSNetMsgServer* thisServer = (BTSNetMsgServer*)arg; BList* serverSocketList = thisServer->SocketList(); BLooper* messageReceiver = thisServer->MessageReceiver(); BMessage* newMessage = NULL; sem_id socketListSem = thisServer->SocketListSem(); long result = B_NO_ERROR; struct timeval tv; struct fd_set readBits; BTSSocket* socket; int i; // Set delay to wait for client data before returning. tv.tv_sec = 1; tv.tv_usec = 0; PRINT(("BTSNetMsgServer::ListenToClients - THREAD ENTER¥n")); for (;;) { // Set the select bits for the known sockets FD_ZERO(&readBits); PRINT(("Acquiring socket list semaphore¥n")); ::acquire_sem(socketListSem); BList socketList(*serverSocketList); ::release_sem(socketListSem); if (socketList.IsEmpty()) goto LOOPEND; for (i = 0; i < socketList.CountItems(); i++) { socket = (BTSSocket*)socketList.ItemAt(i); FD_SET(socket->ID(), &readBits); } // Blocks here until data arrives on any socket. if (::select(32, &readBits, NULL, NULL, &tv) <= 0) goto LOOPEND;
fd_set
構造体である readBits
は、関連あるソケットを見分けるマスクのように働きます。最初、FD_ZERO
によりクリアされ、クライアント・ソケットが一つづつマスクに加えられます。すべてのソケットが加えられると、readBits
を select()
にパラメタの一つとして渡します。select()
がすることは、ソケットの一つにデータが到着するか、タイムアウトするまで待つことです(タイムアウトの時間は、最後のパラメタとして渡されるtv
構造体の値に依ります)。今度の場合ではその値は1秒です。
Select
は、またソケットや sideband communication への書き込みが安全になるまで(すなわちそのソケットに他の操作が何も起こらなくなるまで)待つことにも使用できます。これらについては今は気に掛けないで下さい。これらの機能は
DR8 には組み込まれていません。( writeBits
は DR9 では既にサポートされています。)
さて、 select
が負でない値を返すと、データが到着したのです。データを取り、それを
BMessage
に変換し、サーバーに割り付けられているメッセージ・レシーバにポストしたいと思います。しかし、その前にマスクの中でどのソケットがデータを持っているのかを理解しなければなりません。select()
から戻る時、読まれるためのデータを持たないソケットに対応するビットは消し、データを持つもののビットは残します。クライアント・ソケットの各々について、
FD_ISSET
を呼ぶと、ビットがセットされているかどうかが分かります。ビットがセットされているならば、the
BMessage
を作成し、それをポストします。
PRINT(("Server received data¥n")); for (i = 0; i < socketList.CountItems(); i++) { socket = (BTSSocket*)socketList.ItemAt(i); // Did data arrive on this socket? if (!(FD_ISSET(socket->ID(), &readBits))) goto SOCKETEND; // Yes..assume flattened BMessage format and go get it result = ReceiveNetMessage(*socket, &newMessage); if ( result != B_NO_ERROR ) goto SOCKETEND; // Post it to the message receiver. messageReceiver->PostMessage(newMessage);
ReceiveNetMessage
は、データを受け取り、それを BMessage
に変換するユーティリティ関数です。その中を眺めることにしましょう。もし何かうまく行かないときはどうなるでしょう。ReceiveNetMessage
は、クライアント・ソケットが死んでいると判断したときは、ECONNABORTED
エラーを返すように設計されています。これが起こると、サーバーにメッセージをポストし、この問題があることを知らせます。
// Clean up before checking next socket. SOCKETEND: if (result == ECONNABORTED) { // Inform the server that a client went away. BMessage* deadMessage = new BMessage(DEAD_CONNECTION_MSG); BMessenger* messenger = new BMessenger(thisServer); BMessage* reply = NULL; PRINT(("Connection aborted¥n")); if (deadMessage != NULL && messenger != NULL) { if (deadMessage->Error() == B_NO_ERROR) { deadMessage->AddObject(SOURCE_SOCKET, (BObject*)socket); if (deadMessage->Error() == B_NO_ERROR) { messenger->SendMessage(deadMessage, &reply); } else delete deadMessage; } else delete deadMessage; } if (messenger != NULL) delete messenger; if (reply != NULL) delete reply; } result = B_NO_ERROR; }
クライアントがいないことを指示するメッセージは、単にポストするのではなく、メッセンジャー経由で送られることに注意して下さい。これは、同期コールを送ることになりますが、それによって、次回
ListenToClients()
スレッドがソケット・リストを走査する前に、サーバーがそのソケットをソケット・リストから取り除くことを確実にします。ネットワーク関係の関数を実行するときに、死んでいるソケットを使うとアプリケーションは間違いなくおかしくなります。
もう一つ、データの受け取りをこのようにすることについての注釈をします。select()
が複数個のソケットを含むマスクを付けて呼ばれると、そこで複数個のスレッドが生まれます。これは少し効率の悪いやり方です。もっとよい実装は、個々のソケットごとにスレッドを作り、それらがすべて同じメッセージ・リシーバとサーバーにデータをポストするようにすることでしょう。
クライアントにデータを送るのに、サーバー・オブジェクトにそれをポストし、サーバーはそれを
MessageReceived()
メソッドで取扱います。サーバーが受け取ったメッセージがコントロール・メッセージでないならば、メッセージがTARGET_SOCKET
を含むかどうかによって、特定のクライアントに送るか、すべてのクライアントに送るかのどちらかになります。
default: BTSSocket* socket; if (inMessage->HasObject(TARGET_SOCKET)) { socket = (BTSSocket*)inMessage->FindObject(TARGET_SOCKET); SendNetMessage(*socket, inMessage); } else if (!fClientSocketList.IsEmpty()) { long result = SendToClients(inMessage); } break;
SendNetMessage()
については、次のセクションで眺めます。
SendToClients()
は、単に現在接続されているクライアントの各々のために
SendNetMessage()
を呼びます。
ここまでの議論は一般的なサーバーについてです。ドロー・サーバーはこのサーバーのインスタンスを作成し、クライアントからドロー・メッセージを受け取り、すべての他のクライアントにそれを転送します。それに加えて、ドローイングのローカルなビットマップを積み上げて行きます。これは新しいクライアントが接続したときに、現在のビットマップのコピーを送ることができるようにするためです。興味があれば、BTSDrawServer.cpp
を今チェックして下さい。
クライアントもソケットを作成しなければなりません。サーバーの場合と異なり、それはローカル・ポートにバインドする(ソケット番号をネットワークアドレスに結び付ける)のではなく、サーバーが接続されているリモート・ポートにバインドされます。また、接続を待機したり(listen)、受け付けたり (accept)せず、単に一つのサーバーに接続します(connect)。そこで、クライアントを立ち上げる手続きは次の通りです。
ソケットの作成は、サーバーの場合と全く同じです。BTSNetMsgClient
のコンストラクタが、BTSSocket
を作成します。
ローカル・アドレスにバインドする代わりに、クライアントはリモート・アドレスに接続します。
BTSSocket
はこれを、Connect()
メソッドで行います。後者は、関数
connect()
を呼びます。connect()
はブロックする関数ですから、それを直接呼ぶ代わりに、クライアントが走っている時、クライアントをサーバーに接続するのに別のスレッドを開始します。connect
が成功すると、クライアントは、そのことを BMessage
経由で知らされ、スレッドは閉じられます。
long BTSNetMsgClient::ConnectToServer(void* arg) { // Static function that runs in a separate thread. His whole purpose // is to connect to a server, then he goes away. This prevents the // main client thread from blocking if a server isn't immediately // available. BTSNetMsgClient* thisClient = (BTSNetMsgClient*)arg; BTSAddress address = thisClient->Address(); BTSSocket socket = thisClient->Socket(); int result; // Specify server connection info. result = socket.ConnectToAddress(address); if (result >= 0 && !(thisClient->IsExiting())) { // Since we connected ok, create a socket handler for the // client socket. Also, notify client that we are connected. thisClient->PostMessage(CONNECT_MSG); PRINT(("connected to server!¥n")); } exit_thread(result); }
サーバーの場合と同様に、クライアントも入力データを待ち受けるのに別のスレッドを走らせます。このことは、クライアントがソケット接続成功のメッセージを受け取った後、
BTSNetMsgClient::MessageReceived()
の中で起こります。このスレッドは静的なメソッド
ListenToServer() を走らせます。
long BTSNetMsgClient::ListenToServer(void* arg) { BTSNetMsgClient* thisClient = (BTSNetMsgClient*)arg; BLooper* messageReceiver = thisClient->MessageReceiver(); BMessage* newMessage = NULL; struct fd_set readBits; struct timeval tv; long result; BTSSocket socket = thisClient->Socket(); int socketID = socket.ID(); bool exit = FALSE; tv.tv_sec = 1; tv.tv_usec = 0; while (exit == FALSE) { FD_ZERO(&readBits); FD_SET(socketID, &readBits); if (::select(socketID + 1, &readBits, NULL, NULL, &tv) > 0) { if (FD_ISSET(socketID, &readBits)) { PRINT(("BTSNetMsgClient::ListenToServer - SERVER MSG on %d¥n", socketID)); result = ReceiveNetMessage(socket, &newMessage); if (result == B_NO_ERROR && newMessage != NULL) { // Post it to the message receiver. messageReceiver->PostMessage(newMessage); } else if (result == ECONNABORTED) { // Connection has died if (!thisClient->IsExiting()) { thisClient->PostMessage(DEAD_CONNECTION_MSG); } exit = TRUE; } } } if (thisClient->IsExiting()) { exit = TRUE; } } exit_thread(result); }
これはサーバーの場合とよく似ていますが、この場合はソケット・デスクリプタ一つだけでselect
を呼びます。
それ以外はクライアントはサーバーと同様に動作します。クライアントにメッセージをポストするとメッセージはネットワーク経由でサーバーに渡されます。またサーバーからネットワーク経由でメッセージを受け取ります。クライアントは、サーバーと同じユーティリティとソケット・クラスを使用します。
ドロー・クライアントはBTSNetMsgClient
のインスタンスを作り、ドロー・ビューから受け取ったドロー・コマンドをそれに送ります。他のクライアントからのメッセージは受け取られ、ドローイングのためのウィンドウにポストされます。ドロー・メッセージ自身は、引くべき線の起点と終点に当たる二つのBPoint
のデータだけを含んでいます。ただそれだけです。
ユーティリティ関数は、ネットワーク・データを得てそれをBMessage
に変換して戻すことと、その逆を受け持ちます。
メッセージをネットワーク経由で送る仕事は、BMessage
のFlatten()
と Unflatten()
メソッドの存在によって非常に簡単になっています。しかし
unflatten については、注意しなければならないことがあります。DR8.2 では、正常でないバッファを
unflatten しようとすると、アプリケーション自体ををクラッシュさせることになるでしょう。この点は
DR9 では改善されていますが、目下のところSafeUnflatten
の使用によってこの問題を回避します。
BMessage* SafeUnflatten(const char* buf) { // Safely unflattens a buffer back into a BMessage. Basically, just // check for proper message data header ('PARC') before unflattening. BMessage* newMessage = new BMessage(); PRINT(("SafeUnflatten - ENTER¥n")); if (buf == NULL) return NULL; if (newMessage == NULL) return NULL; if ((!(strcmp(buf, "PARC")) && newMessage->Error() == B_NO_ERROR)) { newMessage->Unflatten(buf); if (newMessage->Error() != B_NO_ERROR) { delete newMessage; newMessage = NULL; } } else if (newMessage != NULL) { delete newMessage; newMessage = NULL; } PRINT(("SafeUnflatten - EXIT¥n")); return newMessage; }
正常なメッセージ・バッファは、"PARC" で始まっているはずです。もう一つ、William が言っているように、「これらエラー関数はオプションではありません(必須です)」。メッセージを扱っているとき、これらが至る所に現れるので、使わずに済ませたいと思われるでしょう。しかしこれらは必要なのです。
unflatten で使われたデータを受け取るのに、難しい問題はありません。ソケットに到着するデータは、先ずメッセージ・バッファ・サイズで、メッセージ・バッファがそれに続きます。
BMessage
データがネットワークに渡されるとき、最初にlong
の識別子、次にメッセージ・バッファ・サイズを表すlong
の値、最後にバッファ自体が送られます。ReceiveNetMessage
はこれらを受け取り、 SafeUnflatten
で締めくくります。
long ReceiveNetMessage(const BTSSocket& socket, BMessage** outMessage) { BMessage* newMessage = NULL; // Holds new message char* buf; // Message data buffer long msgSize; // Message size long result; // Result of socket calls long msgID = -1; PRINT(("ReceiveNetMessage - ENTER¥n")); // Get the header identifying a message. socket.RecvLock(); result = socket.Recv((char*)&msgID, sizeof(long)); if (result == B_NO_ERROR && msgID == MSG_IDENTIFIER) { // Get the message size. result = socket.Recv((char*)&msgSize, sizeof(long)); msgSize = ntohl(msgSize); // Convert from network to native format. if (msgSize >= 0 && result == B_NO_ERROR) { buf = (char*)malloc(msgSize); if (buf != NULL) { // Get the message data. result = socket.Recv(buf, msgSize); if (result == B_NO_ERROR) { // Convert data back into a BMessage. newMessage = SafeUnflatten(buf); if (newMessage != NULL) { // Add an identifier of where it came from. newMessage->AddObject(SOURCE_SOCKET, (BObject*)&socket); } else result = B_ERROR; } free(buf); } } else if (msgSize > 0) result = B_ERROR; } socket.RecvUnlock(); *outMessage = newMessage; PRINT(("ReceiveNetMessage - EXIT¥n")); return result; }
ReceiveNetMessage
とは逆に、 SendNetMessage
はソケットを通してBMessage
を送り出します。BMessage
の
Flatten()
メソッドが使われます。それに続きユーティリティ
SendNetMessageData()
を呼びます。その結果作られるバッファを開放するのはわれわれの責任であることに注意してください。
long SendNetMessage(BTSSocket* socket, BMessage* inMessage) { // Converts a BMessage into a buffer and sends it over the specified socket. char* buf = NULL; long numBytes; long result = B_NO_ERROR; PRINT(("SendNetMessage - ENTER¥n")); inMessage->Flatten(&buf, &numBytes); if (numBytes > 0 && buf != NULL) { result = SendNetMessageData(socket, numBytes, buf); } if (buf != NULL) free(buf); PRINT(("SendNetMessage - EXIT¥n")); return result; }
ネットワーク・メッセージ・データを送り出す一般的なルーチンです。先ず識別子、次にメッセージ・バッファ・サイズ、それからバッファ自身を送ります。ネットワーク・データはBTSSocket::Send()
メソッドを使って送ることができますが、これは network kit のsend()
関数を呼びます。DR8 では、send()
は、すべてのデータが送り出されるか、ソケットが割り込まれるまで常にブロックします。
long SendNetMessage(const BTSSocket& socket, BMessage* inMessage) { // Converts a BMessage into a buffer and sends it over the specified socket. char* buf = NULL; long numBytes; long result = B_NO_ERROR; PRINT(("SendNetMessage - ENTER¥n")); inMessage->Flatten(&buf, &numBytes); if (numBytes > 0 && buf != NULL) { result = SendNetMessageData(socket, numBytes, buf); } if (buf != NULL) free(buf); PRINT(("SendNetMessage - EXIT¥n")); return result; }
送り出しのとき、ソケットをロックするのにセマフォーが使われます。これは複数の送り出しが同時に起こって、メッセージ・データが他のメッセージと混ざってしまわないようにするためです。しかし、ソケットでセマフォーを使う時は注意しないとトラブルに巻き込まれます。その理由は次の通りです。
クライアントとサーバーが同時に相手に送信し、どちらも相手側の受信バッファより大きなバッファを送ろうとしたとします。もし各ソケットがソケットの活動を「すべて」ロックするセマフォーを持つとすると、そのセマフォーは送信の間保たれます。ところが、送信されたバッファは受信バッファより大きいので、送信は終了前に割り込みが掛かります。データの残りを送ろうとする試みは、受信側がこれまで送られたデータを受け取りデータの残りのための場所ができるまで、何度やっても失敗します。しかし双方ともソケット指定のセマフォーを保留しているのならば、どちら側もデータの受け取りができず、デッドロック状態になります。これがこの例では、送信と受信で別のセマフォーを使用する理由です。
これまで述べてきたネットワーク・クラスを使うのに、このセクションを読む必要はありません。しかし組み込まれているネットワーク関数をもっと詳しく知りたい人は読み続けて下さい。
From BTSSocket::Recv()
:
long BTSSocket::Recv(const char* buf, const long bufSize) const { // Receives a network data buffer of a certain size. Does not return until // the buffer is full or if the socket returns 0 bytes (meaning it was // closed) or returns an error besides EINTR. (EINTR can be generated when a // send() occurs on the same socket. long result = B_NO_ERROR; // error value of socket calls int receivedBytes = 0; int numBytes = 0; PRINT(("SOCKET %d RECEIVE: ENTER ¥n", fID)); while (receivedBytes < bufSize && (result == B_NO_ERROR || result == EINTR)) { PRINT(("Receiving %ld bytes on %d¥n", bufSize- receivedBytes, GetID())); numBytes = ::recv(fID, (char*)(buf+receivedBytes), bufSize - receivedBytes, 0); if (numBytes == 0) { result = ECONNABORTED; break; } else if (numBytes < 0) { PRINT(("error when receiving data!¥n")); result = errno; } else { receivedBytes += numBytes; #if DEBUG UpdateReceiveCount(numBytes); #endif } } PRINT(("SOCKET %d RECEIVE - Received %ld bytes result is %s¥n", fID, numBytes, strerror(result))); if (result == EINTR && receivedBytes == bufSize) result = B_NO_ERROR; PRINT(("SOCKET %d RECEIVE: EXIT¥n", fID)); return result; }
このメソッドは二つのことを示しています。第一に受信(Recv)は割り込まれることがあるということです。エラーが発生するか、あるいは同じソケットに誰か他の人が何かを行ったときです。ソケットが誰かによって割り込まれたときは、recv
は、バッファ・サイズより少ないバイト数のデータを返します。それからバッファの残りを取り出すためにループしなければなりません。(このことが起こるとerrno
はEINTR
を返します。)バイト数がゼロより小さく、errno
がEINTR
でないときは、何か悪いことが起こったのです。返されたバイト数がゼロのときは、ソケット接続が閉じられていることを表します。
errno
の値は recv()
が -1 を返したときのみ意味を持ちます。正の数か
0 が戻って来たときは意味がありません。
long BTSSocket::Send(const char* buf, const long bufSize) const { // Sends the data for a BMessage over a socket, preceded by the message's // size. long result = B_NO_ERROR; int numBytes = -1; int sentBytes = 0; PRINT(( "SOCKET SEND - ENTER, %ld bytes on socket %d¥n", bufSize, fID)); if (bufSize > 0 && buf != NULL) { while (sentBytes < bufSize && result == B_NO_ERROR || result == EINTR) { PRINT(("SOCKET SEND - Sending data..¥n")); numBytes = ::send(fID, buf, bufSize, 0); if (numBytes < 0) result = errno; if (numBytes > 0) sentBytes += numBytes; if (sentBytes < numBytes) result = errno; #if DEBUG else UpdateSendCount(sentBytes); #endif } } PRINT( ("SOCKET SEND - EXIT, sent %ld bytes on %d result is %s¥n", sentBytes, fID, strerror(result))); return result; }
受信の場合と同様に、send
もすべてのデータを送り終わったことを確かめなければなりません。そうでなければループして残りを送る必要があります。このことは通常一回の転送で受け取る方のバッファの容量以上に送ろうとするために起こります。
DR8 のネットワーキングには二三問題があります。具体的には、一つのソケットに同時に send や receive が起こるとロックアップを起こす可能性があるということです。select() を呼ぶことがこのバグを引き起こすことに注意して下さい。すなわち、select でブロックされているときに、その select にあるソケットに send しようとするとバグが見えるでしょう。このことは多くの人が同時にドローするときにサーバーに起こり、サーバーをストップさせます。この問題は DR9 では解決されています。
プロトコルとしてBMessage
を使用することは、速度、サイズの観点から非常に効率が高いとは言えません。ネットワークを集中的に使うアプリケーションでは、あなた自身で低レベル・プロトコルを実装したいと思われることでしょう。しかし、アプリケーション間で互いに通信する簡単なネットワーキングがほしい時は、ここのやり方が手早く手軽で、あなたが既に書かれた部分とうまく結合できることと思われます。
あなたのアプリケーションで何がどのように進行しているかについて情報を得るには
ps
を使うことができます。例として、サーバー一つとクライアント一つが走っているときは、次のようなものが得られるでしょう。
DrawServer (team 26) 201 DrawServer sem 10 6 25 rAppLooperPort(13421) 205 w>Draw Server sem 15 7 5 Draw Server(13519) 207 sem 10 0 1 LooperPort(13535) 208 Server Request Handler sem 110 0 15 tcp_receive[201][0](13539) 209 Client Listener sem 110 0 0 tcp_receive[208][1](13792) DrawClient (team 27) 216 DrawClient sem 10 7 43 rAppLooperPort(13658) 224 sem 10 0 0 LooperPort(13760) 226 Client socket listener sem 10 0 1 tcp_receive[216][0](13776) 228 w>Net Draw Client sem 15 6 5 Net Draw Client(13806) net_server (team 14) 72 net_server sem 10 6 11 LooperPort(9064) 86 net main sem 10 4 16 timeout wait(9082) 93 ether reader sem 15 2 8 mace read(9181) 94 socket server msg 10 0 2 95 ether thread sem 10 5 1 etherwait1(9204) 96 loopip thread sem 10 0 0 loop wait(9205) 97 timeout thread sem 10 0 0 timeout cancel(9081) 104 sock:4253,4254 sem 10 0 0 tcp_send[103][0](9349) 109 sock:4326,4328 sem 10 0 0 tcp_send[108][0](9546) 206 sock:5879,5880 sem 10 0 0 tcp_send[201][0](13537) 222 sock:5963,5964 sem 10 0 1 tcp_send[216][0](13774) 225 sock:5965,5966 sem 10 1 1 tcp_send[208][1](13790)
サーバーの tcp_receive
の二つのスレッドがブロックされています。一つ(209)は実際には
select
で、もう一つ((208) は accept
でブロックされているのですが、このように表示されます。クライアントも一つのスレッド(226)がselect
でブロックされています。セマフォー名(訳注例:top_receive)の後に二つの数がブラケットに囲まれていますが、最初の数は、そのソケットを作ったスレッドの識別番号(訳注例:201)、二番目はソケット・デスクリプタ(訳注例:0)です。
net_server では、サーバーとクライアントに関連する三つのスレッド(206, 222, 225) があり、それぞれが(活動中かどうかは別として)「生きた」ソケットを表します。
二つのクライアントが開かれると、そのことがサーバーのselect
に与える影響は
(スレッドを産むこと)、(スレッド 209, 284, and 285)に見られます。それでマルチ・ソケットの
select
はかなり効率が悪いことが分かります。
DrawServer (team 26) 201 DrawServer sem 10 6 26 rAppLooperPort(13421) 205 w>Draw Server sem 15 7 5 Draw Server(13519) 207 sem 10 1 2 LooperPort(13535) 208 Server Request Handler sem 110 0 31 tcp_receive[201][0](13539) 209 Client Listener sem 110 44 37 select sem(21244) 284 select thread sem 10 0 0 tcp_receive[208][1](13792) 285 select thread sem 10 0 0 tcp_receive[208][2](21012) net_server (team 14) 72 net_server sem 10 6 11 LooperPort(9064) 86 net main sem 10 22 47 timeout wait(9082) 93 ether reader sem 15 9 34 mace read(9181) 94 socket server msg 10 0 4 95 ether thread sem 10 26 4 etherwait1(9204) 96 loopip thread sem 10 1 1 loop wait(9205) 97 timeout thread sem 10 20 25 timeout cancel(9081) 104 sock:4253,4254 sem 10 0 0 tcp_send[103][0](9349) 109 sock:4326,4328 sem 10 0 0 tcp_send[108][0](9546) 206 sock:5879,5880 sem 10 0 0 tcp_send[201][0](13537) 222 sock:5963,5964 sem 10 19 34 tcp_send[216][0](13774) 225 sock:5965,5966 sem 10 34 35 tcp_send[208][1](13790) 262 sock:8878,8879 sem 10 0 1 tcp_send[256][0](20965) 265 sock:8880,8881 sem 10 1 1 tcp_send[208][2](21010) DrawClient (team 27) 216 DrawClient sem 10 7 43 rAppLooperPort(13658) 224 sem 10 0 0 LooperPort(13760) 226 Client socket listener sem 10 25 32 tcp_receive[216][0](13776) 228 w>Net Draw Client sem 15 6 5 Net Draw Client(13806) DrawClient (team 29) 256 DrawClient sem 10 7 42 rAppLooperPort(20743) 264 sem 10 0 0 LooperPort(20940) 266 Client socket listener sem 10 0 1 tcp_receive[256][0](20967) 268 w>Net Draw Client sem 15 2 1 Net Draw Client(21026)
ソースには同じ基本的なクライアント・、サーバー・、ソケット・クラスを使った簡単なテキスト・メッセージ交換のプログラム(chat client and server)も入れてあります。
コピーライト【著作権】GBe社,1997年 Beは登録商標です。そして、BeOS、BeBox、BeWare、GeekPort、BeロゴとBeOSロゴはBe社の商標です。文中で述べた全ての他の商標は、それらの所有者の所有物です。 Be, Inc のサイトについてコメントがあれば、 webmaster@be.comへ書いて下さい。 ここで使われたアイコンは、Be社の所有物です。 不許複製。 Copyright 1997 Be, Inc. Be is a registered trademark, and BeOS, BeBox, BeWare, GeekPort, the Be logo and the BeOS logo are trademarks of Be, Inc. All other trademarks mentioned are the property of their respective owners. Comments about this site? Please write us at webmaster@be.com. Icons used herein are the property of Be Inc. All rights reserved.
日本語訳:エスアイ エレクトロニクス(yamagata@sie.or.jp) |