Lesson 19:無謀な挑戦。通信?!

 テキストエディタの作成も、色々と使いにくい面が残ったままにはなっていますが、一応終了しました。
 そこで、次に何をやろうかと考えて、なぜか通信に挑戦してみようかと思い立ってしまいました。
 通信と一口に言っても、シリアルポートやモデム等の制御も通信だし、インターネットの基本技術でもあるTCP/IPも通信です。今回は、TCP/IPに挑戦して見たいと思います。目標はPOP3のサーバにアクセスして、メールの着信状態をチェックできるようになるまでとします。

 今回からの通信の部分は、当然ですが通信環境がなければなりません。端末は一台でかまいませんので、LANカードが刺さった端末を準備し、NetworkでBeOSの通信環境を構築しておいてください。モデムやTAを使ってPPPを使用することもできますが、ちょっと危険ですからあまりお勧めできません。

 最近のOSはほとんどがそうですが、BeOSも標準でTCP/IPをサポートしています。BeBookをみると、そのためのクラスも用意されているようです。
 しかし、今回はクラスは使わず、ソケットという方法を使って実現したいと思います。(BSDやLinux、Windows等の色々なOSでサポートされていますので)
 TCP/IPについて詳しく説明をするには、私の知識では足りないですし、時間もかかってしまいますので、それについては大雑把な説明だけにさせていただきます。

 今回は、クライアントからサーバへの接続と、一回データを送受信するところまでやってみようと思います。
 普段からインターネットではWWWブラウザやFTPクライアント、SMTP/POP3のメールクライアント等を使用していると思いますが、これらがどのように動作しているかを考えたことはありますか? 実はここに上げた三つはどれも似たような手順で通信を行なっているのです。
 これらのアプリケーションは、すべてクライアント/サーバのクライアント側になります。(そりゃ、FTPクライアントとかメールクライアントっていうんだからあたりまえなんだけどね。) TCP/IPでクライアント/サーバ間の通信を行なうのに使用する方法にTCPというものがあります。
 TCPでの通信方法は、接続待ち状態になっているサーバに対して、クライアントから接続要求を出し、サーバ側と接続、通信路を確保します。実際のデータのやり取りはサーバとクライアントが接続した状態の時だけ行なうことができます。
 TCP/IPではサーバやクライアントといった個々の端末を判断するのに使用するのがIPアドレスです。IPアドレスとは192.168.0.1といった255までの数を四つ、ピリオドで区切って表記する値です。(16進数がわかる人なら、これが4バイトの値だということがすぐわかるでしょう。)
 端末にIPアドレスを自分でふっている方は当然、端末のIPアドレスは把握しているでしょうが、DHCPを使用して取得している片は、Networkで端末のIPアドレスを確認してみてください。
 Terminalを起動したら、「ping 端末のIPアドレス」とpingコマンドを実行してください。
 Timed outやUnknown host等といった表示であれば、おそらく通信環境の構築がうまくできていません。通信環境の設定を再確認してみてください。
 通信環境を構築してあるのが前提なのに、なにこんなくだらないことを説明してるんだと思われる方もいらっしゃるとは思いますが、これが意外と理解していない人が多いんですよ。←ソフト開発の仕事の経験談(笑)

 さてと、いよいよプログラムに入っていきますが、久しぶりにBaseAppのプロジェクトから新規プロジェクト「Be6thApp」を作成してください。
 一本のプログラムでサーバとクライアントの両方の機能をいれてしまいます。そこで、メインの画面には、クライアントとの接続待ち状態にする「Listen」ボタン、サーバへの接続を行なう「Connect」ボタン、データの送信を行なう「Send」ボタンと、送信内容を記述するテキストコントロール、情報を表示するストリングビューを配置します。

/**** ファイル名 : MainWindow.h ****/

#ifndef MAINWINDOW
#define MAINWINDOW
//---------------------------------------------------------------------
#include <Be.h>
#include <be/net/socket.h>
//---------------------------------------------------------------------
#define MAINWINDOW_TITLE            "Network"
#define MAINWINDOW_POSITION_LEFT    100
#define MAINWINDOW_POSITION_TOP     100
#define MAINWINDOW_POSITION_WIDTH   400
#define MAINWINDOW_POSITION_HEIGHT  128
#define MAINWINDOW_WINDOWSTYLE      (B_TITLED_WINDOW)
//---------------------------------------------------------------------
#define MSG_CONNECT 'cnnb'
#define MSG_LISTEN  'lstb'
#define MSG_SEND    'sndb'
//---------------------------------------------------------------------
class BAppMainView : public BView
{
public:
    BStringView *strview;
    BTextControl *textctrl;
    BButton *connectbtn;
    BButton *listenbtn;
    BButton *sendbtn;
    BAppMainView(BRect frame);
};
//---------------------------------------------------------------------
class BAppMainWindow : public BWindow
{
public:
    BAppMainView *mainview;
    //-----------------------------------------------------------------
    BAppMainWindow(BRect frame,const char *title);
    virtual void MessageReceived(BMessage *msg);
    //-----------------------------------------------------------------
    bool QuitRequested();
    //-----------------------------------------------------------------
    void sock_connect(void);
    void sock_listen(void);
    void sock_send(void);
};
//---------------------------------------------------------------------
#endif

/**** ファイル名 : MainWindow.cpp ****/

//---------------------------------------------------------------------
#include "MainWindow.h"
//---------------------------------------------------------------------
BAppMainWindow::BAppMainWindow(BRect frame,const char *title)
    :BWindow(frame,title,MAINWINDOW_WINDOWSTYLE,0)
{
    mainview=new BAppMainView(Bounds());    
    AddChild(mainview);
}
//---------------------------------------------------------------------
void BAppMainWindow::MessageReceived(BMessage *msg)
{
    switch(msg->what)
    {
        case MSG_SEND:
            sock_send();
            break;
        case MSG_CONNECT:
            sock_connect();
            break;
        case MSG_LISTEN:
            sock_listen();
            break;
        default:
            BWindow::MessageReceived(msg);
    }
}
//---------------------------------------------------------------------
BAppMainView::BAppMainView(BRect frame)
    :BView(frame,"bappmainview",B_FOLLOW_ALL,B_WILL_DRAW)
{
    BRect viewrect(Bounds());
    connectbtn=new BButton(BRect(viewrect.right-144,8,
                                 viewrect.right-80,28),"connectbtn",
                           "Connect",new BMessage(MSG_CONNECT),
                           B_FOLLOW_RIGHT | B_FOLLOW_TOP);
    AddChild(connectbtn);
    listenbtn=new BButton(BRect(viewrect.right-72,8,
                                viewrect.right-8,28),"listenbtn",
                          "Listen",new BMessage(MSG_LISTEN),
                          B_FOLLOW_RIGHT | B_FOLLOW_TOP);
    AddChild(listenbtn);
    sendbtn=new BButton(BRect(viewrect.right-72,98,
                              viewrect.right-8,118),"sendbtn",
                        "Send",new BMessage(MSG_SEND),
                        B_FOLLOW_RIGHT | B_FOLLOW_TOP);
    AddChild(sendbtn);

    textctrl=new BTextControl(BRect(8,38,viewrect.right-8,58),"textctrl",
                              NULL,"Default",NULL,
                              B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP);
    AddChild(textctrl);

    strview=new BStringView(BRect(8,68,viewrect.right-8,88),"strview",
                            "NoData",
                            B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP);
    AddChild(strview);

    textctrl->SetEnabled(false);
    sendbtn->SetEnabled(false);
}
//---------------------------------------------------------------------
void BAppMainWindow::sock_connect(void)
{
}
//---------------------------------------------------------------------
void BAppMainWindow::sock_listen(void)
{
}
//---------------------------------------------------------------------
void BAppMainWindow::sock_send(void)
{
}
//---------------------------------------------------------------------
bool BAppMainWindow::QuitRequested()
{
    be_app->PostMessage(B_QUIT_REQUESTED);
    return true;
};
//---------------------------------------------------------------------

 「Connect」ボタンを押した際の処理をsock_connect関数、「Listen」ボタンを押した際の処理をsock_listen関数、「Send」ボタンを押した際の処理をsock_send関数に記述するようにしました。

 まずはサーバ側を接続する部分まで作ってみましょう。
 最初のほうでも書きましたが、今回のプログラムではソケットというものを使用します。ソケットはsocket関数で取得するファイル記述子です。取得にはsocket関数、クローズにはclosesocket関数を使用します。ソケット通信では、このファイル記述子を制御することで通信を行ないます。
 ソケットを作成するには、
int ssock = socket(AF_INET,SOCK_STREAM,0);
と記述し、int型の値として取得します。
 ここでAF_INET、SOCK_STREAM等といった定数を使用していますが、TCPで使用するソケットを作成するには、こう書くものだと思ってください。(ここの説明すると、ながくなるので)
 サーバ側では、クライアントからの接続要求を待って、接続要求がきたら、クライアントとの接続を行ないます。
 もう少し細かく書くと、まずソケットを作成したら、そのソケット(ファイル記述子)に接続要求待ちをする条件を設定します。次に接続要求の受付を開始し、接続を待ちます。ソケットの作成にはsocket関数、条件の設定にはbind関数、接続要求の受付を開始するのはlisten関数、接続待ちと接続するにはaccept関数を使用します。

 bind関数は、作成したソケットに受け入れるIPアドレスとポート番号を設定します。
 設定は、sockaddr_in構造体に記述し、それをbind関数の引数として渡します。sockaddr_in構造体の設定とbindの実行は、次のように行います。
struct sockaddr_in sa;
sa.sin_family=AF_INET;
sa.sin_port=htons(10000);
sa.sin_addr.s_addr=INADDR_ANY;
memset(sa.sin_zero,0,sizeof(sa.sin_zero));
bind(ssock,(struct sockaddr *)&sa,sizeof(sa)); 
 sin_familyには必ずAF_INETを指定します。sin_portには使用するポートを指定します。
 TCP/IPの通信では、IPアドレスで通信先を指定しますが、実際にはそのIPアドレスを持つ端末にはいくつもの通信用の口があり、その口をポートといいます。つまり、通信先を指定するということは、IPアドレスとポートの組み合わせで行うことになります。当然、サーバ側も、接続されるためのポートを準備しておかなければなりません。
 ここでは10000番のポートを使用します。htonsマクロはバイトの並び順を変更するためのマクロです。
 2バイト以上の値にはバイトが上位バイトから順にメモリに格納されているか(ビッグエンディアン)、下位バイトからメモリに格納されているか(リトルエンディアン)の二種類があります。
 インターネット等の世界ではビッグエンディアンが一般的に使用されます。ところがIntel系のCPUでは、リトルエンディアンが使用されます。このため、ネットワーク−ホスト間で値を受け渡す際にバイトの並び順を変更する必要があります。
 ntohsマクロはネットワーク側からホスト側へのshort型の値の変更、ntohlマクロはネットワーク側からホスト側へのlong型の値の変更を行ないます。htons、htonlマクロは、ホスト側からネットワーク側への変換を行ないます。
 sin_addrには更にメンバ変数でs_addrがあり、ここで接続を許可するIPアドレスを指定しますが、普通は来るものは拒まずですから、すべて許可するINADDR_ANY定数を指定します。INADDR_ANY定数はIPアドレス0.0.0.0と同じです。
 あとはsin_zeroをすべて0でうめて、bind関数を実施します。bind関数に渡す際にsaのアドレスをsockaddr構造体のポインタにキャストして渡していることに注意してください。

 次にlisten関数で、接続を許可し、実際の接続待ちと接続はaccept関数で行います。
listen(ssock,5);
sa_size=sizeof(sa);
nsock=accept(ssock,(struct sockaddr *)&sa, &sa_size); 
※nsockは、BAppMainWindowクラスのprivateメンバとして定義しておきます。型はint型です。

 listen関数は、bindしたソケットを渡すだけです。accept関数は、bindした条件の接続要求が来るまで待ち、接続要求があると、その接続元の情報を返し、実際に通信を行うための新しいソケットを作成します。二番目の引数は、受け入れるクライアントの数です。

 これで接続待ちまではできるようになっているはずですので、次にクライアントも接続する部分まで作ってみます。

 クライアントの手順はサーバよりも単純で、ソケットを作成したら、接続先を指定して接続するだけです。
struct sockaddr_in sa;
int r;

nsock=socket(AF_INET, SOCK_STREAM, 0);
sa.sin_family=AF_INET;
sa.sin_port=htons(10000);
sa.sin_addr.s_addr=inet_addr("127.0.0.1");
memset(sa.sin_zero,0,sizeof(sa.sin_zero));
r=connect(nsock,(struct sockaddr *)&sa,sizeof(sa));
 ソケットはsocket関数で作成します。サーバと同じように、sockaddr_in構造体で接続のための情報をセットします。ここでサーバと違うのはsin_addr.s_addrに接続先のアドレスを指定していることです。
 inet_addr関数はxxx.xxx.xxx.xxxと記述されているIPアドレスの文字列をDWord型に変換する関数です。ここで使用している127.0.0.1というアドレスは、ループバックアドレスという特殊なアドレスで、自分自身の端末を指します。(INADDR_ANYと同じようにループバックアドレスも定数があるのですが、ここでは使用していません)
 connect関数は指定された情報で接続要求を出します。この関数は接続されるか、タイムアウト等の異常を検出すると返ってきます。異常時には-1が返ってきます。

 接続がうまく行くかを確認するために、接続ができた場合は、用意しておいたストリングビューに「Connect」と表うじするようにしておきましょう。
if(r!=-1)
{
    mainview->strview->SetText("Connect");
}

 ここまで作成したら、実際に動かしてみましょう。実行するとアプリケーションが起動されますが、通信相手が必要となりますので、プロジェクトを保存しているディレクトリにBeAppという実行ファイルができあがっているはずですので、それを通信相手のアプリとして起動します。

 まずどちらかの「Listen」ボタンを押し、サーバを準備します。次にもう一方のアプリケーションの「Connect」ボタンを押して接続してみます。正常に接続されると、「Connect」ボタンを押したほうのアプリケーションに「Connect」と表示されます。
 うまくいかなかったら、もう一度プログラムをみなおしてください。

 次にデータの送受信を行なってみます。クライアントから送信して、サーバで受けるようにします。
 まずはサーバ側から。
 データを受信するには、受信待ちのループを作り、実際に受信があったときだけ受信処理をするようにします。
 受信待ちを行なうにはselect関数を使用します。select関数は特定の事象が起きるか、設定した時間をオーバーするまで待ちつづける関数です。
 タイムアウトする時間は、timeval構造体に設定します。事象による終了かタイムアウトによる終了かの判断は、fd_set構造体を使用します。fd_set構造体のクリアはFD_ZEROマクロを使用します。fd_set構造体にソケットを登録するにはFD_SETマクロを使用します。
 以下に受信待ちと受信部分の例を記述します。
do
{
    tv.tv_sec=2;
    tv.tv_usec=0;

    FD_ZERO(&fds);
    FD_SET(nsock,&fds);

    select(32,&fds,NULL,NULL,&tv);
} while(FD_ISSET(nsock,&fds)==0);

rec=recv(nsock,buf,sizeof(buf),0);
if(rec<=0)
{
    closesocket(nsock);
    nsock=-1;
    strcpy(buf,"Disconnect");	
}

mainview->strview->SetText(buf);
※tvはtimeval構造体、fdsはfd_set構造体です。

 do-whileループが受信待ち部分です。tvに2秒をセットし、fdsにnsockを登録しています。
 select関数には5つの引数を設定しますが、受信待ちに必要な部分は、1番目、2番目、5番目です。関係の無い3、4番目にはNULLを渡します。
 1番目の引数には普通32をセットします。二番目の引数には受信イベントを判断するためのfd_set構造体、5番目にはタイムアウトの時間を設定したtimeval構造体を渡します。
 selectから抜けたらFD_ISSETマクロでnsockで受信があったか否かを判断します。0が返ってきている間は受信していませんのでループを続行します。
 実際にデータを受信するのはrecv関数を使用します。recv関数の引数は、ソケットと格納先バッファのアドレスと大きさを指定します。4番目の引数には必ず0を渡します。
 受信した内容はストリングビューに表示します。

 最後に送信部分を作成します。
void BAppMainWindow::sock_send(void)
{
    int r;
        
    r=send(nsock,mainview->textctrl->Text(),
           strlen(mainview->textctrl->Text())+1,0);
    if(r==-1)
    {
        closesocket(nsock);
        nsock=-1;
        mainview->strview->SetText("Disconnect");
    }
}
 送信は、send関数を使用します。
 send関数には、recv関数と同じで、ソケットと送信バッファのアドレス、その大きさを渡します。4番目の引数に必ず0を渡すのも同じです。

 これで送受信の大まかな説明は終わりです。実際の動作は最後のソースリストを参考にしてください。

 次は、もう少しスマートに送受信できる用に挑戦してみます。


ソースリスト
圧縮ファイル
R5 Intel環境で確認
be6thapp20000918.zip
ソースファイル BaseApp.h
main.cpp
MainWindow.cpp
MainWindow.h

次の項目へ

トップページへ戻る