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 の BLooperBMessage オブジェクトについて、よく知らない方は、これらのセクションをまず読んで下さい。

ネットワークにつながっていなくても、心配御無用です。BeOS に組み込まれているループバックアドレス 127.0.0.1 をあなたのアドレスとして使用することができ、すべてうまく動きます。


コードの構成

チュートリアルのすべてのネットワーキングの基礎に二つのクラス、BTSSocketBTSAddressがあります。 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() メソッドにより、直列化されます。メッセージ・サーバーには、全部で次の三つのスレッドがあります。

  1. BLooperの下位クラスとして作成されたサーバー。これがメッセージをクライアントに送りたい時にポストする宛先のスレッドです。
  2. 接続ハンドラー。クライアントからの接続要求を待ち続けています。
  3. クライアント・リスナー。クライアントからの入力データを待ち続けています。

ドローイング・サーバーは、また他のアプリケーションと同様にBApplicationBWindow スレッドを作成します。

サーバー起動の準備

サーバーは、ある特定のポート番号を指定し、自分をそれに結び付けます。そしてクライアントが接続するのを待ちます。クライアントは、サーバーに接続できるためには、前以ってそのポート番号を知らなければなりません(あるいはサービス名でもよい。これはポート番号のエイリアス-別名-です)。

サーバーが正式に機能するまでにいくつかの関門を通過しなければなりません。このプロセスは 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 で行います。

下位レベルで、この情報を使って接続するのに必要なステップは、

  1. socket() 関数を使ってソケットを作成する。
  2. bind() 関数を使ってソケットをアドレスに結び付ける。
  3. 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 オブジェクトも作られますが、これは familyporthostname をカプセル化し、アドレスに関連するホスト名を与えるなどの変換を行う便宜的なクラスです。作られたソケットはサーバーがクライアントの接続を待つために使用するソケットです。

ソケットが作られたら、これをバインドし、クライアントからの接続待機を始めなければなりません。これは、サーバーのスレッドが 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によりクリアされ、クライアント・ソケットが一つづつマスクに加えられます。すべてのソケットが加えられると、readBitsselect()にパラメタの一つとして渡します。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)。そこで、クライアントを立ち上げる手続きは次の通りです。

  1. ソケットを作成する。
  2. サーバーに接続する。

ソケットの作成は、サーバーの場合と全く同じです。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に変換して戻すことと、その逆を受け持ちます。

SafeUnflatten

メッセージをネットワーク経由で送る仕事は、BMessageFlatten()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 で使われたデータを受け取るのに、難しい問題はありません。ソケットに到着するデータは、先ずメッセージ・バッファ・サイズで、メッセージ・バッファがそれに続きます。

ReceiveNetMessage

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;
}

SendNetMessage

ReceiveNetMessageとは逆に、 SendNetMessage はソケットを通してBMessage を送り出します。BMessageFlatten() メソッドが使われます。それに続きユーティリティ 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;
}

SendNetMessageData

ネットワーク・メッセージ・データを送り出す一般的なルーチンです。先ず識別子、次にメッセージ・バッファ・サイズ、それからバッファ自身を送ります。ネットワーク・データは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;
}

送り出しのとき、ソケットをロックするのにセマフォーが使われます。これは複数の送り出しが同時に起こって、メッセージ・データが他のメッセージと混ざってしまわないようにするためです。しかし、ソケットでセマフォーを使う時は注意しないとトラブルに巻き込まれます。その理由は次の通りです。
クライアントとサーバーが同時に相手に送信し、どちらも相手側の受信バッファより大きなバッファを送ろうとしたとします。もし各ソケットがソケットの活動を「すべて」ロックするセマフォーを持つとすると、そのセマフォーは送信の間保たれます。ところが、送信されたバッファは受信バッファより大きいので、送信は終了前に割り込みが掛かります。データの残りを送ろうとする試みは、受信側がこれまで送られたデータを受け取りデータの残りのための場所ができるまで、何度やっても失敗します。しかし双方ともソケット指定のセマフォーを保留しているのならば、どちら側もデータの受け取りができず、デッドロック状態になります。これがこの例では、送信と受信で別のセマフォーを使用する理由です。


BTSSocket

これまで述べてきたネットワーク・クラスを使うのに、このセクションを読む必要はありません。しかし組み込まれているネットワーク関数をもっと詳しく知りたい人は読み続けて下さい。

ソケットのデータを受け取る

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 は、バッファ・サイズより少ないバイト数のデータを返します。それからバッファの残りを取り出すためにループしなければなりません。(このことが起こるとerrnoEINTR を返します。)バイト数がゼロより小さく、errnoEINTR でないときは、何か悪いことが起こったのです。返されたバイト数がゼロのときは、ソケット接続が閉じられていることを表します。

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)

SIE ホームページに戻る