2016年1月2日土曜日

android - ラズパイ間でのソケット通信

新年一発目はラズパイとアンドロイドでの通信!
C++言語(サーバ側)と、Java言語(クライアント)の入り乱れたややこしい分野から開始

ソケットとは

よく聞く通信プロトコルにTCP/IPがあると思う。
これはインターネット通信をする時に必要なプロトコルで、この通信をプログラムで使うには
TCP/IPとプログラムを結びつける口が必要になる。
その口がソケットと呼ばれるものであり、この通信をソケット通信とも呼ぶ

今回のお題は
サーバ側、クライアント側にそれぞれソケットという通信の口を作り、それをTCP/IPでつなげて通信を行う
具体的には、サーバで入力した文字をクライアント側で表示させる

サーバ側の作り

サーバはラズパイを使う
適当にディレクトリを作って、cppファイルを作る
今回は「SocketTest.cpp」て名前でする
ソケットを作って通信する流れは下記な感じ
  1. ソケット作成
  2. まずはソケット作りから、
    socket()関数により、ソケット作成。作ったソケットのハンドルをs1(int型)で受ける
    // ソケットの作成
    if ((s1 = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
      cout << "ソケット作成失敗" << endl;
      return -1;
    } else {
      cout << "ソケット作成成功" << endl;
    }
    
  3. ソケットのデータ設定
  4. ソケットアドレス(sockaddr_in構造体)を初期化、設定を行う
    // ソケットアドレスの初期化と設定
    memset((char*) &server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    
  5. 1,2で作ったソケットとデータを結びつける
  6. bind()により、ソケットとデータの関連つけを行う
            // 作成したソケットと、アドレスを関連つける(バインド)
    if (bind(s1, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
        cout << "バインド失敗" << endl;
        return -1;
    } else {
        cout << "バインド成功" << endl;
    }
    
  7. コネクションの受信準備
  8. listen()により、何個のクライアントと通信するの?とかの受信準備を行う
    // クライアントからの接続の受付準備をする
    if (listen(s1, 1) < 0) {
        cout << "受付準備失敗" << endl;
        return -1;
    } else {
        cout << "受付準備成功" << endl;
    }
    
  9. コネクションの受信待機
  10. accept()により、コネクトしたらクライアントのデータがclient_addrに格納され、クライアント側のソケットハンドルがs2に入る
    // クライアントからコネクション受付を待つ
    unsigned int len = sizeof(client_addr);
    if ((s2 = accept(s1, (struct sockaddr *) &client_addr, &len)) < 0) {
        cout << "コネクト失敗" << endl;
        return -1;
    } else {
        cout << "コネクト成功" << endl;
    }
    
  11. データ送信
  12. write()によってchar配列のデータを送信する。
    // データ送信
    cout << "送信データを入力してください.-> ";
    cin.getline(buf,BUFSIZ);
    write(s2, buf, sizeof(buf));
    
  13. ソケットの終焉
  14. 最後にソケットを閉じる
    // ソケットを閉じる
    close(s1);
    close(s2);
    
まとめるとこんな感じ
/**
* サーバ側。
* ソケット通信にて、サーバで入力した文字をクライアントに送信するプログラム
*/
#include <iostream>
#include <cstdio>
#include <cstring>

#include <unistd.h>    /* read(),write(),close()         */
#include <sys/socket.h>  /* socket(), bind(), listen(), accept() */
#include <netinet/in.h>  /* struct sockaddr_in           */

#define PORT  (23456)
using namespace std;

int main(int argc, char *argv[]) {
    int s1, s2;                  // ソケットのハンドル
    struct sockaddr_in server_addr, client_addr; // ソケットアドレスのデータ構造体
    char buf[BUFSIZ];              // 送信データ

    // ソケットの作成
    if ((s1 = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        cout << "ソケット作成失敗" << endl;
        return -1;
    } else {
        cout<< "ソケット作成成功" << endl;
    }

    // ソケットアドレスの初期化と設定
    memset((char*) &server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 作成したソケットと、アドレスを関連つける(バインド)
    if (bind(s1, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
        cout << "バインド失敗" << endl;
        return -1;
    } else {
        cout << "バインド成功" << endl;
    }

    // クライアントからの接続の受付準備をする
    if (listen(s1, 1) < 0) {
        cout << "受付準備失敗" << endl;
        return -1;
    } else {
        cout << "受付準備成功" << endl;
    }

    // クライアントからコネクション受付を待つ
    cout << "受付開始..." << endl;
    unsigned int len = sizeof(client_addr);
    if ((s2 = accept(s1, (struct sockaddr *) &client_addr, &len)) < 0) {
        cout << "コネクト失敗" << endl;
        return -1;
    } else {
        cout << "コネクト成功" << endl;
    }

    // データ送信
    cout << "送信データを入力してください.-> ";
    cin.getline(buf,BUFSIZ);
    write(s2, buf, sizeof(buf));

    close(s1);
    close(s2);

    return 0;
}

クライアント側の作り

クライアントはAndroid端末を使う
ソケット通信の手段はJavaで用意されてるクラスを使うため、Androidにこだわる必要はないんだけどねw
ソケットの手順はこんな感じ
  1. ソケット作成
  2. Socketインスタンスを生成した時点で、サーバー側は実装手順5「コネクションの受信待機」が完了する
    そのため、Socketインスタンス生成前に、サーバ側は実装手順5「コネクションの受信待機」まで準備しとかないといけない
    もしできてなければ、生成時に、接続先が見つからないException(UnknownHostException)が投げられる
    // ソケットを作成
    Socket connection = new Socket(HOST_NAME, PORT_NO);
    
  3. データを受けれるようにする
  4. ソケットが生成できたら、サーバからのデータを受信する設定をおこなう
    受信データはSocketインスタンスから、InputStreamを取り出し、BufferedReaderで読み込む
    // 受信データを読み込むBufferedReaderを生成
    InputStream istream = connection.getInputStream();
    InputStreamReader isr = new InputStreamReader(istream);
    BufferedReader reader = new BufferedReader(isr);
    
    // 受信するまで待機して、受信したらデータをString型で取得する
    String message = reader.readLine();
    

ひとまず、
ボタンを押すと、ソケット生成⇒繋ぎに行って、データを受信したら内容を表示するアプリを作る!
  • マニフェスト定義
  • ネットワーク通信するので、ネットワーク許可をマニフェストに記載
    <-- AndroidManifest.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.kazuki.socketsample" >
        <application 
              ・
              ・
              ・
        </application>
        <uses-permission android:name="android.permission.INTERNET" />
    </manifest>
    
  • レイアウト定義
  • ソケット生成ボタンと受信テキスト表示Viewを1個ずつ配置する
    <-- activity_main.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="match_parent"
        android:orientation="vertical">
    
        <Button
            android:id="@+id/socket_open"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="SOCKET OPEN"/>
    
        <TextView
            android:id="@+id/socket_message"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="CLICK BUTTON."/>
    
    </LinearLayout>
    
  • 実装
  • Activityに通信処理を書く
    通信処理は以前紹介したAsyncTaskを使った通信処理でする
    ボタンをClickされたとき、AcyncTaskを生成、更新するViewを引数で渡して、通信処理を開始する感じ
    // MainActivity.java
    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // Click処理を実装
            findViewById(R.id.socket_open).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    TextView v = (TextView)MainActivity.this.findViewById(R.id.socket_message);
                    new MyAsyncTask(v).execute();
                    return;
                }
            });
        }
    
        /**
         * ソケット通信を行い、TextViewの更新を行うクラス.
         */
        static class MyAsyncTask extends AsyncTask<Void,Void,String> {
            private static final String HOST_NAME = "XXX.XXX.XX.XX"; // ラズパイのIPアドレス
            private static final int PORT_NO = 23456;                // ソケット通信するポート番号
            final TextView mSocketText;
            public MyAsyncTask(TextView v) {mSocketText = v;}
    
            @Override
            protected void onPreExecute() {
                mSocketText.setText("通信中....");
                return ;
            }
            @Override
            protected String doInBackground(Void[] params) {
                String message = "";
                try (Socket connection = new Socket(HOST_NAME, PORT_NO);
                         BufferedReader reader =
                               new BufferedReader(new InputStreamReader(connection.getInputStream()));) {
                    message = reader.readLine();
                    if(message == null) throw new NullPointerException();
                }catch(NullPointerException e){
                    message = "読み込みデータがないよ(>_<)";
                }catch(ConnectException e){
                    message = "コネクション失敗(>_<)";
                } catch(UnknownHostException e) {
                    message = "ホストが見つかりません(>_<)";
                } catch(SocketException e) {
                    message = "通信エラー(>_<)";
                } catch (IOException e) {
                    message = "読み込み失敗エラー(>_<)";
                }
                return message;
            }
            @Override
            protected void onPostExecute(String message) {
                mSocketText.setText(message);
            }
        };
    }
    
    22,23行目。サーバ側のIPアドレス、サーバ側実装時に設定したポート番号を入力を忘れずに

    通信のキモは、doInBackground()
※上記はjava7の「try-with-resources」文で書いてる。
Java6で実装する場合は「try-cath-finally」文で書き直しがいるので注意(クローズ処理書き忘れ注意)

完成?

実行手順はこんなかんじで
  1. サーバ側を起動させる
  2. ラズパイ上でコンパイル、実行を行う
    $ g++ SocketServer.cpp
    $ ./a.out
    ソケット作成成功
    バインド成功
    受付準備成功
    受付開始...
    
    うまくいかないときはボートがすでに使われてる可能性が高い
    一回実行させて、「Clrt+C」とかで止め、正常クローズされてない的な..
    そういう時は、リブート作戦!く
  3. クライアント側を接続させる
  4. Androidアプリの「SOCKET START」ボタン押下
  5. サーバ側で文字を送る
  6. 受付開始...
    コネクト成功
    送信データを入力してください.-> あけましておめでとうございます!今年もよろしくお願いします!!
    $
    
    あれ?受信文字列の後ろに変なのついてるッ…!
    「A.aeabi $ 6」ってなんだ?
    文字コードあってないのかな?ちょっと調査中。。。

3 件のコメント:

  1. コネクション失敗とはなんですか?
    解決方法ありますか?

    返信削除
    返信
    1. ConnectExceptionの所でしょうか?
      上記のプログラムだと、
      サーバ側が「受付開始...」になっていない時に出ます

      「受付開始...」でもConnectExceptionが出てるのなら
      ExceptionのStackTrace出力させてみて、
      java.net.ConnectException:XXXX
      このXXXの部分にエラーメッセージが出るので、その文言含めてググってみてはいかがでしょうか

      簡単に思いつく要因だと、
      同じネットワークではない、とかかな
      ラズパイ、Android端末両方とも同じWifiに繋げてみる
      とかで解決できるかもしれません

      削除
    2. 返信送れました。同じネットワークにつないだことで解決しました。
      ありがとうございます。

      削除