WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Tech Blog へとリニューアルします!

デベロッパーブログとしては最後のエントリーを担当するCTO村田です。

2016年7月からはじめた「デベロッパーブログ」ですが、本エントリーをもちまして更新を停止いたします。
デベロッパーブログは閉鎖しませんので、今まで掲載した記事はこれからもご覧いただけます。

これによりワンダープラネットが発信するブログが終了するという事ではなく、タイトルのとおり「Tech Blog」としてリニューアルすることにしました!

Tech Blog としてリニューアルした背景や、リニューアルしたことで変わったことにつきましては、
Tech Blog の投稿「Tech Blog としてリニューアル! - WonderPlanet Tech Blog」をご覧ください。

引き続き、よろしくお願いいたします。 tech.wonderpla.net

CocosBuilder 基本的な機能の備忘

クラッシュフィーバーのUIやエフェクトを担当している伊藤です。


クラッシュフィーバーではUIやエフェクトを作る際、CocosBuilderを使用していますが、ウェブでtipsを探すとあまり初心者向けの記事が見当たりません…

 

そこで、CocosBuilderの基本的な機能の備忘を簡単に紹介したいと思います。

 

 

CocosBuilderのインターフェイス 

f:id:itoc:20170829192428p:plain

 

①プロジェクト内のリソースファイル一覧

.ccbファイルをダブルクリックで開くと作業ウィンドウで編集できます。

 

②作業ウィンドウ

UIの組み立てを行う作業ウィンドウです。

 

③パラメータ設定ウィンドウ

パラメータを変更することができます。
▼基本的なCCNode設定画面

f:id:itoc:20170829173258p:plain

(フォントやFXなどの設定をする場合は、別途パラメータが設定が表示されます。)

表示のチェックボックス:画像の表示非表示ができます。
位置:画像の基点を基準指定できます。

サイズ:画像オブジェクトやNodeのサイズを指定できます。

アンカーポイント:基点の位置を指定します。

 X. 0 Y. 0がデフォルト位置。

 X. 0.5 Y. 0.5にすると画像の中心が基点となります。

スケール:拡大縮小です。中心の鍵マークをオンにすると比率が固定になります。

回転:回転できます。

スキュー:画像を斜めに変形できます。

 

④タイムライン

f:id:itoc:20170829174312p:plain

追加したSpriteやNodeが表示されます。

下のレイヤーに行くほど、ビューで手前に表示されます。

タイムラインでは、追加したSpriteやNodeレイヤーにアニメーションをつけることが出来ます。

アニメーションさせたいレイヤーを選択し、タイムラインのバー(青い縦線)を、
動かしたい位置まで移動させて以下のキーを押すと、位置やスケールにアニメーションキーが追加されます。

表示:V

位置:P

スケール:S

回転:R

透明度:O

カラー:C

(もしくは、追加したいタイミングにalt+左クリックで直接キーを追加できます。)

 

⑤作業ウィンドウのズーム

f:id:itoc:20170829173139p:plain

⑭の作業ウィンドウをズームできます。「=」を押すとリセットされます。

 

⑥パネルの表示切り替え

f:id:itoc:20170829173143p:plain

⑬、⑮、⑱のウィンドウの表示、非表示を切り替えられます。

 

 ⑦Nodeの追加

f:id:itoc:20170829173146p:plain

CCNodeを追加します。

CCNodeはフォトショップでいうグループレイヤーのような使い方をしており、

例えば、アニメーションを付ける場合、複数あるレイヤーを纏めて動かしたいときなどに使います。
 
⑧レイヤーの追加

f:id:itoc:20170829173150p:plain

平面レイヤーをつくれます。

左からCCLayer、CCLayerColor、CCLayerGradient、CCScrollView。

f:id:itoc:20170829173154p:plain

 

⑨スプライトの追加

f:id:itoc:20170829173157p:plain

 画像オブジェクトの追加が出来ます。

ボタン左がCCSprite、右がCCScale9Spriteです。 

◆CCSprite:通常の画像追加。

CCSpritを追加すると、パラメータウィンドウに設定画面が表示されますので、Sprite frameで追加したい画像を選択します。
また、opacityで透明度の指定、Colorで色を付けられます。
(CocosBuilderでは画像の白い部分に色が付きます。)
▼CCSpriteの設定画面

f:id:itoc:20170829173201p:plain

Blendでは加算や減算などの合成モードを選択できます。

詳しく知りたい方は下記記事を参照下さい。

developer.wonderpla.net

 

◆CCScale9Sprite:9Sprite画像の追加。

画像を任意のサイズに引き伸ばすことができます。

CCScale9Spriteを追加すると、パラメータウィンドウに設定画面が表示されますので、Preferred sizeとInsetsの値を設定します。

▼CCScale9Spriteの設定画面

f:id:itoc:20170829173205p:plain

・Preferred size:オブジェクトの幅、高さを指定します。

・insets:オブジェクトの一部分を固定し、伸ばしたくない部分を指定します。

▼参考イメージ
設定画面では、左から150px、右から150pxを固定しており、

f:id:itoc:20170829181002p:plain
全体の横幅が540pxに伸ばす設定しています。 

f:id:itoc:20170829180926p:plain

 

⑩ラベルの追加

f:id:itoc:20170829173216p:plain
テキストの追加ができます。 
ボタン左がCCLabelTTF、右がCCLabelBMFontです。

◆CCLabelTTF:システムフォントを追加できます。

▼CCLabelTTFの設定画面

f:id:itoc:20170829173221p:plain

font file:フォントファイルの指定

Font size:文字サイズを指定
opacity:不透明度

color:文字の色
Dimensions:文字が入る領域を指定します

blemd:合成モード選択

LabelText:表示するテキストを入れます


 

◆CCScale9Sprite:ビットマップフォントを追加できます。
▼CCScale9Spriteの設定画面

f:id:itoc:20170829173225p:plain

手書き文字のビットマップフォントの文字サイズはスケールで指定します。


 ビットマップフォントの作り方は下記事にて紹介されておりますので、ご興味があればご確認下さい。

developer.wonderpla.net

⑪メニューの追加

f:id:itoc:20170829173230p:plain


⑫コントロ−ラーの追加

f:id:itoc:20170829173233p:plain

※⑪⑫に関しては殆ど使用していないため割愛致します。

 

⑬FXの追加

エフェクトを追加できます。

パラメータ設定内の Particle Textureで画像オブジェクトを指定すればエフェクトが簡単に作れます。

f:id:itoc:20170829182532p:plain

f:id:itoc:20170829182829g:plain


クラフィではParticleDesignerというツールを使用してParticleエフェクトを作成しているので殆ど使っていない機能ですが、簡単なFXであればかなりお手軽に作れます。

 

 ⑭CCBの追加

f:id:itoc:20170829173244p:plain

別のccbファイルを配置することが事ができます。

 

⑮プラグインの追加

f:id:itoc:20170829173251p:plain

CCAddSprite:スプライトの追加ができます。

CCCameraNode:カメラを配置できます

CCParticleSystemQuadForCCB:plistの追加ができます

UISpriteButton:ボタンを作ることができます。

ここはほぼ使用していないため、詳しい説明は省きますが、

カメラ位置(視点)の指定や、他のツールを使用して作成したパーティクル(.plist)を追加できます。

 

⑯表示ウィンドタブ

現在開いているccbファイルの一覧です。

 

⑰タイムラインの再生・停止

f:id:itoc:20170829183251p:plain

タイムラインのアニメーションを再生できます。

⑱タイムラインの拡大縮小

f:id:itoc:20170829183350p:plain

タイムラインの拡大縮小し、見やすい長さに調整します。


⑲タイムラインの長さの設定
下部分をクリックするとタイムラインの長さを変更出来ます。
f:id:itoc:20170829183548p:plain

クリックすると長さ指定を行うウィンドウが表示されるので、アニメーションの尺に合わせてタイムラインの長さを変更できます。

f:id:itoc:20170829183459p:plain

  

ーーーーー
この記事が、Cocosを使用する方や、これから使用する方の助けになれば幸いです。 

Instant Previewを使ってDaydream対応アプリを効率よく開発する方法

R&D事業部の近藤です 5月のGoogle I/OでDaydreamについての発表がありました。
その中で紹介されていたInstant Previewというデバッグに便利なツールを使ってみたので、今回はそれがどんなものか紹介します。

何ができるツール?

Unity editorで実行すれば、USBで接続したAndroid端末で即実行できるのでデバッグが超便利。
ビルドしてアプリに転送しなくても良いのでトライ&エラーがやりやすくなります。

事前準備

Gooleのドキュメントを参考に、Google VR SDK for UnityをダウンロードしてUnity上でDaydreamが使えるようにしましょう。

[ドキュメント]
・SDKのダウンロード
https://developers.google.com/vr/unity/get-started

・UnityでDaydreamのコントローラを使えるようにする方法
https://developers.google.com/vr/unity/controller-support

端末に専用のアプリをインストールする

Instant Previewを利用するには端末側にも専用のアプリをインストールします。
USBケーブルでPCに端末を接続し、ターミナルで下記のコマンドでインストールします。
「adb install -r InstantPreview.apk」
※事前にadbのパスを通しておきましょう

InstantPreviewのUnityパッケージの入手

Githubからパッケージをダウンロードできます
https://github.com/googlevr/gvr-instant-preview

利用しているGoogle VR SDKのバージョンによって異なるパッケージが用意されています。
ここでは最新版のSDKを利用しているので「InstantPreviewForGVRUnitySDKv1.50.0.unitypackage」を使用します。

「InstantPreview」フォルダの中に「GvrInstantPreviewMain」というプレハブがインポートされます。
これをHierarchyの一番上の階層に配置します。

これでInstant Previewの準備完了です。

f:id:HidehikoKondo:20170626170338p:plain

実行

Instant Previewのアプリをインストールした端末とPCを接続し、Unityの実行ボタンを押しましょう。
2つのモードの選択を求められますので、「Full VR Preview mode」を選択します。
すると、Unity上の画面が端末に反映され、端末の動きに応じて画面がぐるぐる動くようになります。
ビルドの時間を待つ必要が無いのでこれでトライ&エラーがやりやすくなりますね。
ただし、FPSは遅いので、パフォーマンスの確認は実際にアプリにインストールして確認する必要があります。 f:id:HidehikoKondo:20170626181015g:plain

2つのモード

「Full VR Preview mode」
端末の向きに合わせてカメラも動きます。
これで端末に画面を表示しながらエディター側で調整作業をすることができます。
上の動画ではこのモードで実行しています。

「Controller-only」
名前の通り、コントローラーのみがエディター上で使えるようになります。
カメラの向きは動かすことができません。

Daydreamのこれから

Google I/OでInstant Preview以外にもいくつかのツール類が発表されており、GoogleがVRコンテンツの開発をしやすくなるように力を入れていることがうかがえます。
これから開発者向けのツールも、Daydream対応端末も増えてくると思います。
Daydreamはまだ日本国内では発売していませんが、いつか発売されることを期待して待っていたいですね。

オブジェクト指向プログラミングの五つ原則 - S.O.L.I.D (S編)

良いソフトウェアとは何か。

顧客満足を実現するアプリケーション?不具合がないソフトウェア?

難しい質問だと思っておりますが、概して機能の拡張や変更が容易でバグが発生しにくいことに纏められると思います。 かつて、手続き型プログラミングからクラス、メソッド、抽象化などの概念のオブジェクト指向プログラミングを経て関数型プログラミングに至るまで ソフトウェア開発方法論は開発者がより短い時間で様々な機能を少ないミスで開発できるように発展してきました。

今回はオブジェクト指向の五大原則と言われているSOLIDについて話してみましょう。

プログラミングのデザインパターンといえば有名な「GOFパターン」がありますが、SOLID原則はそれよりもさらに基本原則で良いデザインパターンを作成するために 必要なコンセプトだと行っても無理ではないと思います。

S.O.L.I.Dとは?

S - 単一責任の原則 (Single Responsibility Principle)

O - 開放・閉鎖原則 (Open/closed principle)

L - リスコフ置換原則 (Liskov substitution principle)

I - インタフェース分離の原則 (Interface segregation principle)

D - 依存性逆転の原則 (Dependency inversion principle)

1. SRP (単一責任の原則:Single Responsibility Principle)

A class should have one and only one reason to change, meaning that a class should have only one job.

上記の言葉を直訳すると「クラスはただ一つの理由で変更すべきだ。つまり、クラスは一つの機能だけを持っているようにすること。」になります。

SRP原則を適用すれば責任領域が明確になりますので、一つの責任を変更することが次々と他の責任の変更になり続ける連鎖作用を防止できます。

更に、責任を適切に分配することにより、コードの可読性の向上,メインテナンスに容易という利点があります。

それに加えSRP原則は次回紹介するOCP原則を適用する基盤となっています。

活用の例

リファクタリング(Refactoring)を要求する問題はこのSRP原則を考えていないことで発生する場合が多いです。

  • 散弾銃手術(Shotgun surgery) : ショットガンサージャリーはアンチパターン(antipattern)の一つで,新しいサービスや責任をコードベースに追加して 一つの変更に複数な変更が行ってしまうことをいいます。

ショットガンサージャリーコードの例)

void LogFunc()
{
    printf("Entering LogFunc()\n");
    ...
}

void LogFunc2()
{
    printf("Entering LogFunc2()\n");
    ...
}

...

void LogFuncN()
{
    printf("Entering LogFuncN()\n");
    ...
}

SRP BEFORE & AFTER

  • SRP原則適用前
class Guitar()
{
    public Guitar(String serialNumber, double price, Maker maker, Type type,
                  String model, Wood backWood, Wood topWood, int stringNum)
    {
        this.serialNumber = serialNumber;
        this.price = price;
        this.maker = maker;
        this.type = type;
        this.model = model;
        this.backWood = backWood;
        this.topWood = topWood;
        this.stringNum = stringNum;
    }

    private String serialNumber;
    private double price;
    private Maker maker;
    private Type type;
    private String model;
    private Wood topWood;
    private Wood backWood;
    private int stringNum;

    // ...
}

上記の要所の中で「price, maker, type, model, backWood, topWood, stringNum」はギターのタイプによって変更が多い部分だと思います。

この場合,ギターの種類によって変更する必要がない要所(例. serialNumber)あるいはクラス全体を修正しなければならないようになってしまいます。

では、SRP原則を考えてリファクタリングしてみましょう。

  • SRP原則適用後
class Guitar()
{
    public Guitar(String serialNumber, GuitarSpec spec)
    {
        this.serialNumber = serialNumber;
        this.spec = spec;
    }

    private String serialNumber;
    private GuitarSpec spec;

    // ...
}

class GuitarSpec()
{
    double price;
    Maker maker;
    Type type;
    String model;

    // ...
}

上記のように変更が発生した部分を新しいクラスに分けて処理したことが確認できます。

まとめ

クラスは一つのサービスに一つの責任を持たせて、名前はその内容を明確に表すように作成した方は良いと思っております。

ただ、単純に責任で区別することではなくて各機能の凝集力(cohesion)を考慮し、最初からオブジェクトの間にデカップリング(decoupling)する手間が掛かないようにした方が良いですね。

参考資料)Head First Object-Oriented Analysis and Design (O'Reilly 2006.11) - Brett D. McLaughlin, Gary Pollice, David West

C/C++でネットワークプログラミングをやってみよう(2)

2話 ソケットプログラミングを実際にやってみよう

概要

今回は実際にソケットのプログラミングをしてみたいと思っています。
目標ですが、TCPプロトコルで簡単なクライアントとサーバを作成し、ソケットのシステムコールの使い方を覚えたいと思います。
UDPに関しては、「6話 NAT超えをしてみよう(P2P通信)」にて説明する予定です。

今回の流れとして、一旦はシステムコールの呼び出しを一連の流れで実装します。
それから、もう少し細かい設定をしていき、最終的にはライブラリ化を行いたいと思います。
そして、このライブラリを使い、次回は簡単なチャットのクライアント・サーバを作成する予定です。

今回作成するのは、クライアントからサーバへ文字列を送れば、サーバからは少し加工した文字列を返してくれる簡単なものとします。
今回はシステムコールを覚えることに目的があるためです。

クライアント ver1

順番的に

  1. TCP/IP用のディスクリプター生成

  2. サーバへ接続

  3. サーバへデータ送信

  4. サーバからデータ受信

  5. ディスクリプターの解放

TCP/IP用のディスクリプター生成
    int sock=socket(PF_INET,SOCK_STREAM,0);
    if (sock<0)
    {   
        /* TODO ソケットが生成されない場合のエラー処理 */
        printf("socket failure!!! (%d)", errno);
        exit(0);
    }   

socketは、システムコールです。
第一引数PF_INETはipv4のIPプロトコルです。
第二引数はSOCK_STREAMの場合はTCPになります。PF_INETとSOCK_STREAMを組み合わせることで、このディスクリプターはTCP/IPプロトコル上で動作することになります。

サーバへ接続
    const int port=10101;
    const char* ip_address="127.0.0.1";
    struct sockaddr_in addr={0,};

    /* 接続のために構造体に値を入れる */
    addr.sin_family = AF_INET;
    /* 接続先のポート番号。ビッグエンディアンの32ビットに変換*/
    addr.sin_port = htons(port);
    /* inet_addrはxxx.xxx.xxx.xxxの文字列をビッグエンディアン(ネットワークバイトオーダー)の32ビットに変換する。*/
    addr.sin_addr.s_addr = inet_addr(ip_address); 

    /* サーバにコネクトを行う。この関数はブロック型である。
      コネクトのタイムアウトはデバイスドライバーの実装によって違う。
       このタイムアウト値の設定もくせものでOSによっては設定ができない。
       なので、タイムアウトをさせる場合は、別途実装する必要があるが、色々と説明が長くなるので
       ライブラリにまとめる際のコードを参考にして欲しい。 

       第二引数のstruct sockaddrのキャストだが、sockの第一引数に何を指定しているかで指定される構造体が違うためにキャストすることになる。
       現代のソフトウェア工学的には気持ち悪い実装かも知れないが、connectがシステムコールである故にやむを得ないところがある。
    */
    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr))!=0)
    {   
        /* 失敗*/
        printf("socket connect error!!! (%d)", errno);
        exit(0);
    }   

サーバへデータ送信

実際にはsendで転送するのがオススメだが、ファイルの扱いと似ているので、理解のためにwriteで転送している。
実際にはネットワークデバイスではread/writeのインタフェースを持っている。
第二引数はデータのポインターで、第3の引数はデータのサイズである。
今回は文字列を送るので、文字列のサイズ+Zero Terminateの1byteである。

    int write_bytes=write(sock, message, message_size+1);
    if (write_bytes!=message_size+1)
    {   
         /* 失敗*/
        printf("write error!!! (%d)", errno);
         exit(0);
    }

サーバからデータの受信

sockに対して何もしなければ、read/recv関数は受信終了までブロックする。
ただし、例えば、サーバからクライアントに1MByteのデータを送信した際に1MByteを全て受信するまでブロックするわけではない。
read/recvするタイミングで受信した分までかサイズに指定している分ーーサンプルでは1024ーーまで読み込むことになる。
実際に全て受信しきれてない場合は、ループで回りながら受信することになるが、この事に関しては次回もう少し詳しく説明を行う。
今のところでは、以下のような形でデータを受信するという感じをつかめて欲しい。

    char buffer[1024];
    int n=read(sock, buffer, 1024);
    if (n<0)
    {
         /* エラー */
        printf("read error!!! (%d)", errno);
         exit(0);
    }
    if (n==0)
    {
         /* 接続が切れた! */
        printf("connect closed!!! (%d)", errno);
         exit(0);
    }
    printf("%s\n", buffer);

ディスクリプターの解放

    close(sock);

これらの実装はgitのレポジトリーにまとめていますので、参考にしてください。
git@github.com:dongho-yoo/network_programming.git/chapter-2/sample1

サーバ ver1

今度はサーバのサンプルです。
流れは、

  1. TCP/IP用のディスクリプター生成します。

  2. bind ポート番号とディスクリプターを結びつけます。

  3. listenディスクリプターをTCPハンドジェーク用に待ち状態にします。

  4. acceptクライアントと確立した接続からクライアントへのディスクリプターを生成します。

  5. データのやり取り

TCP/IP用のディスクリプターの生成に関してはクライアントと同じです。

bind

基本的にディスクリプターとポート番号を結びつけます。クライアントも本当はbindすべきですが、クライアントが特定のポートで待ち受けることをしないので、connect関数の中で空いているデフォルトのポート番号を割り当てています。

 36     int port=atoi(argv[1]);
 37     /* ここからがバインド処理 */
 38     struct sockaddr_in addr={0,};
 39     addr.sin_family = AF_INET;
 40     /* 待ち受けるポート番号。ビッグエンディアンの32ビットに変換*/
 41     addr.sin_port = htons(port);
 42     /* アドレスが複数ある場合は指定するが、そうでもない場合は以下のように適当にアドレスを指定する */
 43     addr.sin_addr.s_addr = htonl(INADDR_ANY);
 44     const size_t addr_size = sizeof(addr);
 45 
 46     /* 引数はクライアントのconnectに似ている */                                                                                                                                                                                      
 47     if (bind(sock, (struct sockaddr*)&addr, addr_size)==-1)
 48     {
 49         /* ポートが解放されず、バインドされないケースもある。テスト時はプロセスが落ちたりするので、ポートが解放されない場合がある。
 50            そういう場合は、sockoptでSO_REUSEADDRを使うべき */
 51         printf("bind error!!!(%d)\n",errno);
 52         close(sock);
 53         exit(0);
 54     }

listen

TCPの接続というのは、TCP three-way handshakingを意味します。
以下を参照。
3ウェイ・ハンドシェイク - Wikipedia
実際にはTCPのヘッダーのみのやり取りを3回繰り返す事になります。この threee-way handshakingはデバイスの中で、connectするクライアント側とlisten状態のサーバで行う事になります。
クライアントの説明で、connectのタイムアウト処理を行うのはやや複雑になると話したのはそういう意味です。

 55     /* クライアントの待ち受け
 56      listenの第二引数はbacklogである。筆者がソケットプログラミングを始めた頃では接続がまだ確立されていない状態のクライアントの接続のキューの数だったが、最近は接続確立済みのキューの数になっているようだ。MAN LISTEN(2)を参照
 57      なので、以下の10では、クライアントのコネクション待ち状態は最大10コネクションになり、11番目のコネクションからはコネクトエラーになる。*/
 58     if (listen(sock, 10)==-1)
 59     {
 60         /* エラー */
 61         printf("listen error!!!(%d)\n",errno);
 62         close(sock);
 63         exit(0);
 64     }
 65     printf("listen success!!!\n");

accept

確立したコネクションからディスクリプターを生成します。
この生成されたディスクリプターへ書き込むと実際にクライアント側にデータが送信されます。
ディスクリプターに対して何もしなければ、acceptはブロック型関数になります。

 66 ACCEPT:
 67     /* ディスクリプターsockは、クライアントの待ち受け状態になっている。listenがブロックして待っているわけではない。
 68        acceptでブロックして待つことになる。
 69        connectとbindと引数の指定の仕方が少し違うのに注目。
 70        acceptが成功した場合、accept_addrにはクライアントのipアドレスやポート番号などの値が入ってくる。
 71        */
 72     struct sockaddr_in accept_addr={0,};
 73     socklen_t accpet_addr_size=sizeof(accept_addr);
 74     int sock_client=accept(sock,(struct sockaddr*)&accept_addr, &accpet_addr_size);
 75 
 76     if (sock_client==0||sock_client<0)
 77     {
 78         /* エラー */
 79         exit(0);
 80     }

実行してみよう

一旦、これでクライアントとサーバができました。
以下のレポジトリーをcloneします。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle1

sample1のディレクトリで、makeコマンドを実行します。

 $ make

するとclientとserverの実行ファイルが生成されます。 サーバを実行します。(ポート番号は12345)

 $ ./server 12345 &

クライアントを実行します。

 $ ./client 127.0.0.1 12345 -m hellow

サーバを終了させる場合は、

 $ ./client 127.0.0.1 12345 -m quit

別のマシーンで、サーバを立ち上げてテストしてもOKです。
ただ、サーバ側のファイアウォール設定でサーバに指定したポートが開いていないといけません。

ここまでで、流れは単純な通信を行う流れです。
ただし、少し機能が少ないです。

まず、read/recv時のタイムアウト処理が入っていません。

このような場合、よく使われるのはシステムコールのselectです。

select

システムコールのプロトタイプは以下のとおりです。

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

第一引数:設定しているディスクリプターの最大値+1が入ります。
第二引数:ディスクリプターが読み込む準備ができたかどうかをチェックするディスクリプターのリストーーディスクリプターは全てソケットである必要はなく、selectに対応したデバイスならばOKーー
第三引数:ディスクリプターが書き込み準備ができているかどうかをチェックするディスクリプターのリストーーconnectのタイムアウト時にはこちらを使いますーー
第四引数:ディスクリプターがエラーになっているかどうかのチェックをするディスクリプターのリストだが、実際に使うシチュエーションがあまりない。
第五引数:タイムアウト値の設定。

110     /* ディスクリプターを管理する構造体 */                                                                                                                                                                                                           
111     fd_set fdz;
112     struct timeval tv = {3,0}; /* タイムアウト3秒 */
113 
114     /* このFD_で始まるマクロに注目。
115        実は、OSによってselectの実装が異なるために、fd_setという構造体の型もバラバラである。
116        そのためにFD_で始まるマクロを利用し、使い方を統一している。
117        以下のマクロはfdzにディスクリプターを設定する。
118        実際には複数のディスクリプターを設定できて、どのディスクリプターからの受信したのかが分かるようになる。
119        この際に設定できるディスクリプターのタイプだが、ソケット以外にもselect対応のデバイスは全て一緒に設定できる。
120        その使い方は次回のネットワークフレームワーク作る際に紹介する予定*/
121     FD_ZERO(&fdz); /* fdzの初期化 */
122     FD_SET(sock,&fdz); /* fdzにディスクリプターを設定します。複数ある場合は複数指定できます。 */
123 
133     /* この場合は、ディスクリプターの受信待ちなので、第二引数にのみfdzを設定する */
134     int n=select(sock+1, &fdz, 0, 0, &tv);
135     /* 戻り値は、受信データのあるディスクリプターの数なので、戻り値がゼロならばタイムアウトということになる */
136     if (n==0)
137     {
138         /* タイムアウト */
139         fprintf(stderr,"Timout!!!!\n");
140         close(sock);
141         exit(0);
142     }
143     if (n<0)
144     {
145         /* エラー の場合は、ディスクリプターの不正とみなして良い。*/
146         close(sock);
147         exit(0);
148     }
           /* fdzにsockしか設定していないので、受信データのあるディスクリプターは1個しかない */
149     if (n==1)
150     {
151         /* こんな感じで、どのディスクリプターに受信されたかをチェックできる。
152            このプログラムでは、sockしかないので無意味ではあるが。。。 */
153         if (FD_ISSET(sock,&fdz))
154         {
155             char buffer[1024];
156             int n=recv(sock, buffer, 1024,0); /* データ受信 */
157             if (n<0)
158             {
159                  /* エラー */
160                 printf("read error!!! (%d)", errno);
161                  exit(0);
162             }
163             if (n==0)
164             {
165                  /* 接続が切れた! */
166                 printf("connect closed!!! (%d)", errno);
167                  exit(0);
168             }
169             printf("%s\n", buffer);
170          }
171     }

あとは、サーバの処理が多い場合、処理中の別の接続を受け付けることが出来ないので、別のスレッドで分岐するようにします。

サーバの分岐

acceptしたあと、サンプル1のようにクライアントの受信待ちになると次のacceptができない。selectを使って、listen用のディスクリプターとクライアント用のディスクリプターを両方設定すれば、接続の対応とクライアントの処理を両方行うことはできるが、クライアントの数が多かったり、処理そのものが多かったりすると接続処理に回るまで時間がかかる。なので、accept後はスレッドか子プロセスに分岐するのが一般的である。

カーネルからするとスレッドとプロセスはジョブとしては大した差はない。ただ、スレッドとプロセスの大きな違いはメモリ空間を共有するかであり、スレッドはメモリ空間を共有しているために、メモリ関係のエラーになった場合プロセスごと強制終了ーーカーネルからプロセスを削除ーーされる。ただ、子プロセスは親プロセスを複製しているので、子プロセスがメモリを壊しても親プロセスとは関係ないために子プロセスだけジョブから削除される。

故にサーバプログラミングは子プロセスを使うとサーバがダウンすることはかなり少なくなる。*1

ただし、forkは親プロセスのメモリ空間を複製するために生成にオーバーヘッドがかかるのとメモリの使用率が非常に高くなるので、サーバの目的によって、どのように分岐させるかを工夫すべきである。
サンプルでは、スレッドで分岐している。

 16 static void* peer_connected(void* param)
 17 {
 18     int sockclient=(int)(intptr_t)param;
 19 
 20     char buffer[1024];
 21     if (recv(sockclient,buffer,sizeof(buffer),0)>0)
 22     {
 23         char send_buffer[64];
 24         sprintf(send_buffer, "received %s", buffer);
 25         send(sockclient, send_buffer, strlen(send_buffer)+1, 0);
 26         close(sockclient);
 27 
 28         printf("SERVER: receved %s\n", buffer);
 29    
 30         /* クライアントからのメッセージが"quit"ならば、終了 */                                                                                                                                                                       
 31         if (strcmp(buffer, "quit")==0)
 32         {   
 33             g_quit=1;
 34             break;
 35         }
 36     }
 37     return 0;
 38 }
                                                      :
                                                      :
117         int sock_client=accept(sock,(struct sockaddr*)&accept_addr, &accpet_addr_size);
118 
119         if (sock_client==0||sock_client<0)
120         {
121             /* エラー */
122             exit(0);                                                                                                                                                                                                                 
123         }
               /* スレッド生成 */
124         pthread_t th;
134         if (pthread_create(&th,0,peer_connected,(void*)sock_client)!=0)
135         {
136             /* 各peerに関して、スレッド終了待ちをする必要がないので */
137             pthread_detach(th);
138         }

ここまでのサンプルは、以下のレポジトリーにあります。実行方法はサンプル1と同じです。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle2

説明したものをまとめてライブラリ化します。TCP/IPに特化したライブラリにします。
こうすることでネットワークプログラミングがしやすくなります。
ライブラリにまとめる際に少し細かい設定などを行なっていますので、ソースを参考にしてください。
サンプル2との違いは、connectの非同期処理でタイムアウトができるようにしたり、send時のSIGPIPE対応などが入っています。
実はソケットプログラミングはこのような細かな処理を行うのが大変かもしれませんが、ライブラリとしてまとめておけば、そこからは気にすることはありません。

wp_tcp_scok.h

/**                                                                                                                                                                                                                                      
* Copyright (c) 2017 WonderPlanet Inc. All Rights Reserved.
* Authorized by dongho.yoo.
*/

#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif

/** @brief ソケットのディスクリプターの型を定義 */
typedef int sock_t;
typedef int addr_ipv4_t;

/* 一度に読み込むバッファーのサイズ。システムによって最適な値がある。これは実証値を使うべきだが、一旦4KBytesで。 */
#define WRITE_ONCE_BYTES (4096)

/* 一度に読み込むバッファーのサイズ */
#define READ_ONCE_BYTES (4096)

/**
* @brief サーバへ接続します。
* @param ipaddress 文字列のIPアドレス
* @param port ポート番号
* @param timeout タイムアウト(ミリ秒)、timeoutがゼロの場合はブロック
* @return ソケットのディスクリプター
*/
extern sock_t wps_connect_s(const char* ipaddress, int port, int timeout);
/**
* @brief サーバへ接続します。
* @param ipaddress ビッグエンディアンの32ビットのipv4のipアドレス
* @param port ポート番号
* @param timeout タイムアウト(ミリ秒)、timeoutがゼロの場合はブロック
* @return ソケットのディスクリプター
* @retval -1 ソケット生成失敗
* @retval -2 タイムアウト
*/
extern sock_t wps_connect(addr_ipv4_t ipaddress, int port, int timeout);
/**
* @brief データを送信します。
* @param data 送信するデータ
* @param data_size 送信するデータサイズ
* @return 送信したデータのバイト数
*/
extern int wps_send(sock_t sock, const void* data, const size_t data_size);
/**
* @brief データを受信します。
* @param data 受信するバッファー
* @param data_size 受信するバッファーサイズ
* @param timeout ミリ秒、ゼロの場合はブロック
*/
extern int wps_recv(sock_t sock, void* buffer, const size_t buffer_size, int timeout);
/**
* @brief リッスン状態のソケットを生成します。
* @param ipaddress IPアドレス、NULLの場合はデフォルトのIPアドレスで取得。
* @param port 待ち受けるポート番号
* @param backlog TCPハンドジェーク終了時のクライアントをacceptまで貯められる数
* @param force 1の場合は、ポートがすでに使われていたり、解放されてない場合にも強制的にバインドする
* @return ソケットディスクリプター
*/
extern sock_t wps_listen(const char* ipaddress, int port, int backlog, int force);
/**
* @brief 接続を確立させて、クライアントとのディスクリプターを生成する
* @param sock_for_listen リッスン状態のディスクリプター
* @param ip_address 受け取るクライアントのipアドレス(NULLの場合は無視される)
* @param port 受け取るクライアントのport番号(NULLの場合は無視される)
*/
extern sock_t wps_accept(sock_t sock_for_listen, addr_ipv4_t* ip_address, int* port);
/**
* @brief ディスクリプターを削除します。
* @param sock ディスクリプター
*/
extern void wps_close(sock_t sock);
/**
* @brief ソケットの受信を待ちます。
* @param sock ディスクリプター
* @param timeout タイムアウト
* @return 0ならタイムアウト
*/
extern int wps_wait(sock_t sock,int timeout);
#ifdef __cplusplus
}
#endif

wp_tcp_scok.c

/**                                                                                                                                                                                                                                      
* Copyright (c) 2017 WonderPlanet Inc. All Rights Reserved.
* Authorized by dongho.yoo.
*/
#include "wp_sock.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <errno.h>

#define PRINT_ERROR(...) fprintf(stdout,__VA_ARGS__)

static sock_t tcp_sock()
{
    sock_t sock;
#ifndef MSG_NOSIGNAL
    /* BSD系はこれを指定しないと、write時にサーバ側からデータがくるとシグナルが発生して、
    プロセスが終了する。*/
    signal(SIGPIPE, SIG_IGN);
#endif

    if ((sock= socket(AF_INET, SOCK_STREAM, 0))<0)    
    {   
        PRINT_ERROR("socket create error !");
        return -1; 
    }   
    return sock;
}
/** ディスクリプターをノンブロックする関数*/
static void sock_non_block(sock_t sock, int nonblock)
{
    fcntl(sock, F_SETFL, nonblock?O_NONBLOCK:0);
}
static int is_valid_sock(int sock)
{
    int error=0;
    socklen_t len;

    if (sock==0)
    {   
        return 0;
    }   
    /* ソケットが無効化どうかをチェックする */
    if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len)!=0)
    {   
        return errno;
    }   

    if (error==0 ||
            error==EISCONN)                                                                                                                                                                                                              
    {   
        return 1;
    }   

    PRINT_ERROR("socket invalid reason:%d", error);
    return 0;
}
static int is_ready_connect(int sock, int timeout)
{
    int res=0;
    fd_set fds;
    struct timeval tv={timeout/1000,(timeout%1000)*1000};

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

    /* selectでディスクリプターが書き込み可能になるまで待つ。
       MAN CONNECT(2)を参照した。
    */
    res = select(sock+1, 0, &fds, 0, &tv);

    if (res<0)
    {
        PRINT_ERROR("select error errno:%d", errno);
        return res;
    }

    if (res==0)
    {
        return 0;
    }
    if (FD_ISSET(sock, &fds)!=0)
    {
        if (is_valid_sock(sock))
        {
            return 1;
        }
    }

    return 0;
}
/** connectのタイムアウト版 
    複雑に見えるが、実際にはTCPのHandshake終了待ちを手動で行っているだけ。
*/
sock_t wps_connect_s(const char* ipaddress, int port, int timeout)
{
    return wps_connect(inet_addr(ipaddress),port,timeout);
}
sock_t wps_connect(addr_ipv4_t ipaddress, int port, int timeout)
{
    int sock;
    struct sockaddr_in addr={0,};                                                                                                                                                                                                        
    int val;
    int retryCnt=timeout/1000;

#ifndef MSG_NOSIGNAL
    /* BSD系はこれを指定しないと、write時にサーバ側からデータがくるとシグナルが発生して、
    プロセスが終了する。*/
    signal(SIGPIPE, SIG_IGN);
#endif

    if ((sock=tcp_sock())<0)
    {
        PRINT_ERROR("socket create error !");
        return -1;
    }

    if (timeout!=0)
    {
        val = 1;
        /*  タイムアウトにする場合には、一旦ソケットをノンブロック状態にする。*/
        sock_non_block(sock, 1);
   timeout=timeout/1000; /* 秒数に変える */
    }

    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=ipaddress;

__RETRY:
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr))!=0)
    {
        if (timeout==0)
        {
            PRINT_ERROR("connect error!\n");
            return -1;
        }

        if (errno==EAGAIN)
        {
            /* ノンブロック時に発生するエラーでこれは仕様なのでリトライ*/
            goto __RETRY;
        }
        if (errno==EINPROGRESS)
        {
            /* まだ書き込み可能な状態でない場合のエラー */
            int res;
__WAIT:
            /* この中で書き込み可能になるまで待つ。1秒タイムアウト*/
            res = is_ready_connect(sock, 1000);
            if (res<0)
            {
                PRINT_ERROR("connect error... (%d)", errno);
                return -1;
            }
            if (res==0)
            {
                if (retryCnt--!=0)
                {
                    PRINT_ERROR("timeout so... retry");
                    goto __WAIT;                                                                                                                                                                                                         
                }
                // retryCnt is zero.
                PRINT_ERROR("connect timeout (%d)", errno);
                return 0;
            }

            /* ノンブロック解除 */
            sock_non_block(sock, 0);
            return sock;
        } /* if EINPROGRESS */
        return 0;
    } /* connect retry or failure. */

    return sock;
}
static int __send(sock_t sock, const void* data, const size_t data_size)
{
#ifdef MSG_NOSIGNAL
    /* 第4引数のMSG_NOSIGNALを指定しないと、こちらが書き込んでいる際にコネクションが切れたり、peer側で書き込みがあった際にシグナルのSIGPIPEが発生してプロセスが落ちてしまう */
    return send(sock,data, data_size,MSG_NOSIGNAL);
#else
    return send(sock,data, data_size,0);
#endif
}
int wps_send(sock_t sock,const void* data,const size_t data_size)
{
    size_t sum=data_size;
    char* p=(char*)data;
    /* WRITE_ONCE_BYTES ずつ書き込む */
    while(sum!=0)
    {
        const size_t size=(sum<WRITE_ONCE_BYTES)?sum:WRITE_ONCE_BYTES;
        int n =__send(sock, p, size);
        p+=n;
        sum-=n;
    }
    return data_size;
}
int wps_recv(sock_t sock, void* buffer, const size_t buffer_size, int timeout)
{
    int sum=buffer_size;
    char* p=(char*)buffer;
    if (timeout!=0)
    {
        fd_set fdz;
        struct timeval tv={timeout/1000,(timeout%1000)*1000};
        FD_ZERO(&fdz);
        FD_SET(sock,&fdz);
        int res = select(sock+1,&fdz,0,0,&tv);
        if (res<0)
        {
            /* エラー */
            return -2;
        }
        if (res==0)
        {
            /* タイムアウト */
            return -1;                                                                                                                                                                                                                   
        }
    }
    /* recvに関しては今の状態でREAD_ONCE_BYTESずつ読み込むのは不可能。
    その理由は、相手が送ったデータのサイズがわからないためである。
    なので、recvの方は、プロトコルの実装のところで、READ_ONCE_BYTESずつ読み込む処理を入れる。*/
    return recv(sock,buffer,buffer_size,0);
}
sock_t wps_listen(const char* ipaddress, int port, int backlog, int force)
{
    sock_t sock;
    struct sockaddr_in addr={0,};
    const size_t addr_size = sizeof(addr);

    if ((sock=tcp_sock())<0)
    {
        PRINT_ERROR("socket create error !");
        return -1;
    }

    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=ipaddress?inet_addr(ipaddress):htonl(INADDR_ANY);

    if (bind(sock,(struct sockaddr*)&addr, addr_size)==-1)
    {
        if ((errno==EADDRINUSE||errno==EACCES)&&force==1)
        {
            /* ポートが解放されなかったり、すでに使われている場合に、force==1の場合は強制的にバインドする */
            int on=1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
            if (bind(sock, (struct sockaddr*)&addr, addr_size)==-1)
            {
                /* これでもダメならお手上げ!*/
                PRINT_ERROR("bind error!!!(%d)\n",errno);
                return -1;
            }
        }
        else
        {
            PRINT_ERROR("bind error!!!(%d)\n",errno);
            close(sock);
            return -1;
        }
    }
    if (listen(sock, backlog)==-1)
    {
        /* エラー */
        PRINT_ERROR("listen error!!!(%d)\n",errno);
        close(sock);
        return -1;
    }

    return sock;
}                                                                                                                                                                                                                                        
sock_t wps_accept(sock_t sock_for_listen,addr_ipv4_t* ip_address, int* port)
{
    struct sockaddr_in accept_addr={0,};
    socklen_t accpet_addr_size=sizeof(accept_addr);
    int sock_client=accept(sock_for_listen,(struct sockaddr*)&accept_addr, &accpet_addr_size);
    if (ip_address!=0)
    {
        *ip_address=accept_addr.sin_addr.s_addr;
    }
    if (port!=0)
    {
        *port=ntohs(accept_addr.sin_port);;
    }

    return sock_client;
}
void wps_close(sock_t sock)
{
    close(sock);
}
int wps_wait(sock_t sock,int timeout)
{
    int res=0;
    fd_set fdz;
    struct timeval tv={timeout/1000,(timeout%1000)*1000};

    FD_ZERO(&fdz);
    FD_SET(sock, &fdz);

    /* 汎用的に待つ処理をするので、読み込み時と書き込み時、両方待つようにする */
    return select(sock+1, &fdz, &fdz, 0, &tv);
}

上記のまとめたライブラリで、サンプル2も実装しています。ライブラリとサンプル2の実装は以下のレポジトリーから取得します。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle3

次回予告

次回は、今回まとめたライブラリを利用して、チャットサーバを作成します。
キーになるのは、構造体を送受信するので、プロトコルの設計やプロトコルのパックの仕方などの説明が入るのと複数が同時に接続をするので、複数のディスクリプターの場合の管理の仕方が焦点となります。
クライアント側は、フレームワーク化をする予定です。
機能はシンプルでも構造的には本格的なものを目指しています。

*1:Apacheもデフォルトではforkで子プロセスを生成している。tomcatのようにvm上で動いているものはスレッドでも全体が死ぬことはない。

MaBeee SDKを使ってミニ四駆をスマホでコントロールしよう

R&D事業部の近藤です。 先日、ワンダープラネットで社内ハッカソンが開催されました。
そのときに発表した、MaBeeeを使ってミニ四駆を音声でコントロールするアプリを作りました。
今回はiOSでのMaBeeeに対応したアプリの作り方を紹介しようと思います。

MaBeeeとは?

f:id:HidehikoKondo:20170817114112p:plain
単3の乾電池型のIoTデバイスで、Bluetoothでスマートフォンに接続し、乾電池の出力をコントロールすることができます。
乾電池の出力の強弱をコントールすることで、おもちゃや家電製品をスマートフォンでできるようになります。
非公式ですがSDKも提供されておりMaBeeeを利用したアプリを自分で作ることもできます。

MaBeeeの製品情報はこちら

https://mabeee.mobi/

SDK

iOS版SDKはこちら

https://github.com/novars-jp/MaBeeeiOSSDK

Android版SDKはこちら(今回は使いません)

https://github.com/novars-jp/MaBeeeAndroidSDK

サンプルコード

Githubにサンプルコードがいくつか公開されています。

https://github.com/novars-jp

MaBeeeを利用したアプリの作り方

まずはXcodeで新規プロジェクトを作りましょう。
今回はSingle View Applicationにしました。

GithubからダウンロードしたSDKを解凍しプロジェクトに「MaBeeeSDK.framework」を取り込みます。 f:id:HidehikoKondo:20170816125353p:plain

実装

Storyboard

ボタンを4つ配置します。 「はやい!」「ゆっくり」「ストップ」とラベルに設定します。
3段階で乾電池の出力をコントロールするように実装します。
tagに設定した値はMaBeeeの出力のコントロール用に利用します。
0~100で設定します。 4つ目の「MaBeeeに接続」ボタンはMaBeeeと接続するための画面に遷移するためのボタンです

f:id:HidehikoKondo:20170816125652p:plain

ボタンtagの設定値動作
はやい!100乾電池の出力100%
ゆっくり50乾電池の出力50%
ストップ0乾電池の出力0%

コード

#import "ViewController.h"

//MaBeeeSDK.frameworkを利用するためのインポート
@import MaBeeeSDK;

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

//MaBeeeの接続設定の画面を開く
- (IBAction)maBeeeScanButtonPressed:(UIButton *)sender {
    MaBeeeScanViewController *vc = MaBeeeScanViewController.new;
    [vc show:self];
}

//接続したMaBeeeの取得して、tagの値(出力)を設定
- (IBAction)speedButton:(UIButton *)sender {
    for (MaBeeeDevice *device in MaBeeeApp.instance.devices) {
        device.pwmDuty = (int)sender.tag;
    }
}

- (IBAction)maBeeeScanButtonPressed:(UIButton *)sender

MabeeeとiPhoneをbluetoothで接続するための画面を開きます。
Storyboadで「Mabeeeに接続ボタン」とIBActionの接続をしておきましょう。 f:id:HidehikoKondo:20170816130159p:plain

- (IBAction)speedButton:(UIButton *)sender

Storyboardで配置した3つの出力調整用のボタンとこのメソッドをIBActionで接続します。
ボタンのtagに設定した値を出力のコントロールするための値として利用します。 f:id:HidehikoKondo:20170816130213p:plain

実行

電源オン

まずはMaBeeeを装着してミニ四駆の電源スイッチをONにします。
この時点ではまだ電池の出力が0の状態なのでミニ四駆は動きません。
f:id:HidehikoKondo:20170817114332p:plain

MaBeeeとiPhoneの接続

「MaBeeeに接続」ボタンを押すとMaBeeeとの接続画面が開き、電源の入っているMaBeeeを検索してくれます。
見つかったMaBeeeをタップすれば接続完了です。
f:id:HidehikoKondo:20170817114906p:plain

走行!!

接続ができたら「はやい!」「ゆっくり」「ストップ」のボタンを押してみましょう。
MaBeeeを組み込んだミニ四駆が3段階の速さで動き出します!

まとめ

アプリの実装次第では、加速度センサーや音声入力をトリガーにして操作をすることもできます。
アイデア次第でとっても面白い使い方ができるデバイスだと思います。
ちなみに社内ハッカソンではApple WatchからiPhoneを経由して操作できるようにしてみました。
MaBeeeを利用した作品のコンテストなども開催されていますし、ハッカソンのようなイベントに持ち込んで作品を作ってたら楽しいかもしれないですね。

社内ハッカソンの様子はWantedlyのこちらの記事で紹介しています。 www.wantedly.com

C/C++でネットワークプログラミングをしてみよう(1)

目的

筆者の専門分野でもあり、最近は色んなネットワークのライブラリーーcurl、photonのようなーーが多いので、手作りのクライアントやサーバも少なくなっています。 そのために問題が生じてもどのような問題なのか分からず、ライブラリーのパッチをいつまでも待たないといけなくなったり、効率の悪い回避策を入れたりすることになります。
しかしながら、オープンソースが今のように発展していない一昔は、こういうライブラリは自前で作ってきたものです。
ライブラリーを使うのに慣れている現在のエンジニアさんには難しく思えるのかも知れませんが、実際にポイントを押さえていれば、そこまで難しいものではありません。
この連載によって、基本的なソケット通信のやり方を覚え、実戦で応用ができるならばいいと思っています。

対象

 ・ソケットプログラミングに興味のある方
 ・Photonなどのネットワークフレームワークやライブラリの内部構造が気になる方
 ・ハッキング?に興味のある方

内容

1話 TCP/IPにおけるOSIの参照モデルについて
2話 ソケットプログラミングを実際にやってみよう
3話 プロトコルを実装してみよう(HTTP編)
4話 OpenSSLをつかったSSL通信をしてみよう
5話 PROXYサーバを作ってみよう
6話 NAT超えをしてみよう(P2P通信)

1話 TCP/IPにおけるOSIの参照モデルについて

概要

ネットワークプログラミングに興味のある方は、一度は耳にしたことがあると思います。
このOSI参照モデルというのは、ISOで策定されたコンピュータの通信機能において、必要であるべき機能を階層化したものです。これは、厳密な規約ではありませんが、ネットワークの実装時に推奨されるモデルです。
今は、ほとんどの通信がTCP/IPプロトコル上で動いているので、こういったモデルの意味も薄れていますが、一昔はZModem、AppleTalk、NetBEUI・・・など無数な通信プロトコルがあったためです。
このモデルの階層は7階層でできており、これをOSI’s 7Layersと言います。
本章では、TCP/IPプロトコルにおける参照モデルを中心に説明していこうと思っています。

階層は以下の通り

  1. ハードウェア層
  2. データリンク層
  3. ネットワーク層
  4. トランスポート層
  5. セッション層
  6. プレゼンテーション層
  7. アプリケーション層

既にご存知の方々もいらっしゃると思いますが、初めての方はこれだけでは何を意味するのか分からないと思うので、これに関して、わかりやすく説明をしたいと思います。

1. ハードウェア層

ある人が面白いことを考えました。
パソコンとパソコンをつないで通信をさしょうと思ったわけです。
そうするためには、送受信ができる装置が必要となります。
この装置にはモデム、ISDN、ケーブルモデム、イーサネットカードなどがありますが、一旦ここではイーサネットカードのみを想定します。
この装置をパソコンに入れてパソコンとパソコン同士をつなぎます。
これがハードウェア層となります。

2. データリンク層

さて、パソコンとパソコンを繋いだ状態で、AからBへメッセージを送ろうとします。
イーサネットカードに何かのデータを書き込みと読み取りができないと行けません。
そこで、イーサネットカードのバッファーにデータを書き込み・読み込むモジュールを作成しました。保護モードで動くOSでは、デバイスドライバーに当てはまります。
このドライバーの書き込みや読み込みの機能がデータリンク層*1になります。
基本、データリンク層ではハードウェア層にリクエストを投げることになります。

3. ネットワーク層

ただ、これだけでは面白くありません。今度は複数のパソコンに繋いで見たくなりました。
そうするためには、送り先を指定しなければなりません。
なのでパソコンに住所をつけることにしました。
その一つの規約*2がip protocol (internet protocol)です。
ipアドレスというのは、ipヘッダーの識別子になります。
そういうわけで、デバイスの方にipprotocolを実装し、転送するようにしました。
送信パケットにipヘッダーをつけて、送り先のアドレスと一緒に送信するような感じです。
この場合、送信先までに送るための中継機器が必要となります。一旦、その機器をルーターとしておきます。
一方で、受信する側のパソコンでは、自分のipアドレスのみ*3を受信できるようになります。
このデバイスドライバーのipプロトコルの実装と中継機器がネットワーク層になります。
ちなみにpingはこのネットワーク層までで動いています。
ネットワーク層ではデータリンク層にリクエストを投げるようになります。

4. トランスポート層

これで複数のパソコンで通信ができるようになりました。
でも、もう少し面白いことがしたいところです。
パソコン同士でチャットをしたり、ファイルのやりとりを同時にやりたい。
でも、ipプロトコルだけでは、どれがチャットのメッセージでどれがファイルのメッセージなのか区別がつきません。
そこで工夫された一つの規約がTCPプロトコルです。
ポート番号を設け、デバイスに届くデータを振り分ける仕組みです。
TCPプロトコルをデバイスに実装しました。*4
なので、データを送る際にTCPヘッダー+実データにして、ネットワーク層へ要求を出すことになります。
この機能がネットワーク層とトランスポート層に当てはまります。
このトランスポート層では、データのロスを防ぐために、データが失われた場合は、再要求を送信したりするような機能も実装してあります。
トランスポート層では基本的にネットワーク層にリクエストを投げるようになります。

5. セッション層

これで、パソコン同士が通信ができるようになりました。
ただ、LANケーブルはノイズがあり、データが失われたり、接続が不安定なことが多いです。
その接続の回復などの処理ーー通信の再送とは別ーーを行う機能を追加しており、この処理を行うのがセッション層であります。*5

TCP/IPにおいて、ハードウェア層からセッション層までは全てハードウェアとデバイスドライバーの内部の話になります。
実際にこの階層で綺麗に分かれているような実装になっているかどうかは不明ですが。。。
プレゼンテーション層というのは、アプリ層の方で、プレゼンテーション層へ要求を出す際に形式的な変換ーー例えば、文字コードの変換ーーのような事をしますが、TCP/IPにおいて、プレゼンテーション層というのは、デバイスドライバーの方で対応することが難しいです。
なぜなら、TCP/IPの上に色んなプロトコルが存在するために一概に処理をすることができないためです。
なので、TCP/IPのネットワークプログラミングの話は主にプレゼンテーション層とアプリ層の話になります。
抽象的で分かりづらいとは思いますが、実際にソケットプログラミングをすると分かってくるかと思います。

最後に次回の話のための何を使ってソケット通信のプログラムをするかを説明したいと思います。

Burkey Socket

ネットワークデバイスでは、以下のようなインターフェースを提供しています。
これらはシステムコールで、以下のシステムコールの形式をBurkey Socketと言います。BSD Unixに初めて実装され、今はほとんどのOSで採用されています。

  • socket
    openと同じようにdescriptorを生成します。ファイルと同じくdescriptorはi-nodeとして管理されます。
  • close
    socketで生成されたdescriptorを解放します。
  • send/write
    データを送信します。ファイルと同じくwriteもサポートしていますが、ioctlなどで調整が必要となります。なのでsendを使う方をお勧めします。
  • recv/read
    データを受信します。
  • ioctl/fctl
    ソケットの受信をnon-block状態にしたり、送信時のバッファーを設定したり、ブロードキャストの設定をしたり細かい設定を行うためのインターフェース。 OSの実装の仕方によって、どれをどう使うかになります。ほぼどちらも使えますが、OSによっては片方しか使えなかったりします。
  • select
    設定したdescriptorから何か変化があるまで、スレッドをスリープ状態にします。

ストリーム(TCPのコネクション型時の追加のシステムコール)

  • connect
    リモートと接続*6を計ります。
  • bind
    ポート番号とdescriptorを結びつけます。
  • listen
    クライアントのconnectを待ちます。
  • accept
    listenから接続が確立したセッションに対してdescriptorを生成します。

ソケットプログラミングにおいて、使うのはは上記のシステムコールのみとなります。
今回は文章が多く退屈だったかと思いますが、次回では簡単なクライアントとサーバのプログラムを作成したいと思います。

*1:厳密には、イーサネットカードのヘッダーも含まれる

*2:ipアドレスなしでどのように通信を行うのか疑問に思う方もいると思うので説明すると、そういう場合はMACアドレスやドメイン名などで通信することになります。ただし、ローカル内での通信までしかできない

*3:今のスウィッチングハブは、違うipアドレスの機器には転送しないが、その前まではつながっている全ての機器に送信していたの>で、各機器の方で違うipアドレスのパケットを弾く処理を行なっていた。

*4:デバイスドライバーの実装はここから複雑になっていく

*5:TCP/IPプロトコルに置いては、セッション層はデバイスの方ではそんなに処理がない。AppleTalkのような通信プロトコルでは、かなり多くの機能が入る

*6:ハンドシェーク