2016年1月2日土曜日

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

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

ソケットとは

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

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

サーバ側の作り

サーバはラズパイを使う
適当にディレクトリを作って、cppファイルを作る
今回は「SocketTest.cpp」て名前でする
ソケットを作って通信する流れは下記な感じ
  1. ソケット作成
  2. まずはソケット作りから、
    socket()関数により、ソケット作成。作ったソケットのハンドルをs1(int型)で受ける
    1. // ソケットの作成
    2. if ((s1 = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    3. cout << "ソケット作成失敗" << endl;
    4. return -1;
    5. } else {
    6. cout << "ソケット作成成功" << endl;
    7. }
  3. ソケットのデータ設定
  4. ソケットアドレス(sockaddr_in構造体)を初期化、設定を行う
    1. // ソケットアドレスの初期化と設定
    2. memset((char*) &server_addr, 0, sizeof(server_addr));
    3. server_addr.sin_family = AF_INET;
    4. server_addr.sin_addr.s_addr = INADDR_ANY;
    5. server_addr.sin_port = htons(PORT);
  5. 1,2で作ったソケットとデータを結びつける
  6. bind()により、ソケットとデータの関連つけを行う
    1. // 作成したソケットと、アドレスを関連つける(バインド)
    2. if (bind(s1, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
    3. cout << "バインド失敗" << endl;
    4. return -1;
    5. } else {
    6. cout << "バインド成功" << endl;
    7. }
  7. コネクションの受信準備
  8. listen()により、何個のクライアントと通信するの?とかの受信準備を行う
    1. // クライアントからの接続の受付準備をする
    2. if (listen(s1, 1) < 0) {
    3. cout << "受付準備失敗" << endl;
    4. return -1;
    5. } else {
    6. cout << "受付準備成功" << endl;
    7. }
  9. コネクションの受信待機
  10. accept()により、コネクトしたらクライアントのデータがclient_addrに格納され、クライアント側のソケットハンドルがs2に入る
    1. // クライアントからコネクション受付を待つ
    2. unsigned int len = sizeof(client_addr);
    3. if ((s2 = accept(s1, (struct sockaddr *) &client_addr, &len)) < 0) {
    4. cout << "コネクト失敗" << endl;
    5. return -1;
    6. } else {
    7. cout << "コネクト成功" << endl;
    8. }
  11. データ送信
  12. write()によってchar配列のデータを送信する。
    1. // データ送信
    2. cout << "送信データを入力してください.-> ";
    3. cin.getline(buf,BUFSIZ);
    4. write(s2, buf, sizeof(buf));
  13. ソケットの終焉
  14. 最後にソケットを閉じる
    1. // ソケットを閉じる
    2. close(s1);
    3. close(s2);
まとめるとこんな感じ
  1. /**
  2. * サーバ側。
  3. * ソケット通信にて、サーバで入力した文字をクライアントに送信するプログラム
  4. */
  5. #include <iostream>
  6. #include <cstdio>
  7. #include <cstring>
  8.  
  9. #include <unistd.h> /* read(),write(),close() */
  10. #include <sys/socket.h> /* socket(), bind(), listen(), accept() */
  11. #include <netinet/in.h> /* struct sockaddr_in */
  12.  
  13. #define PORT (23456)
  14. using namespace std;
  15.  
  16. int main(int argc, char *argv[]) {
  17. int s1, s2; // ソケットのハンドル
  18. struct sockaddr_in server_addr, client_addr; // ソケットアドレスのデータ構造体
  19. char buf[BUFSIZ]; // 送信データ
  20.  
  21. // ソケットの作成
  22. if ((s1 = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
  23. cout << "ソケット作成失敗" << endl;
  24. return -1;
  25. } else {
  26. cout<< "ソケット作成成功" << endl;
  27. }
  28.  
  29. // ソケットアドレスの初期化と設定
  30. memset((char*) &server_addr, 0, sizeof(server_addr));
  31. server_addr.sin_family = AF_INET;
  32. server_addr.sin_addr.s_addr = INADDR_ANY;
  33. server_addr.sin_port = htons(PORT);
  34.  
  35. // 作成したソケットと、アドレスを関連つける(バインド)
  36. if (bind(s1, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
  37. cout << "バインド失敗" << endl;
  38. return -1;
  39. } else {
  40. cout << "バインド成功" << endl;
  41. }
  42.  
  43. // クライアントからの接続の受付準備をする
  44. if (listen(s1, 1) < 0) {
  45. cout << "受付準備失敗" << endl;
  46. return -1;
  47. } else {
  48. cout << "受付準備成功" << endl;
  49. }
  50.  
  51. // クライアントからコネクション受付を待つ
  52. cout << "受付開始..." << endl;
  53. unsigned int len = sizeof(client_addr);
  54. if ((s2 = accept(s1, (struct sockaddr *) &client_addr, &len)) < 0) {
  55. cout << "コネクト失敗" << endl;
  56. return -1;
  57. } else {
  58. cout << "コネクト成功" << endl;
  59. }
  60.  
  61. // データ送信
  62. cout << "送信データを入力してください.-> ";
  63. cin.getline(buf,BUFSIZ);
  64. write(s2, buf, sizeof(buf));
  65.  
  66. close(s1);
  67. close(s2);
  68.  
  69. return 0;
  70. }

クライアント側の作り

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

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

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

完成?

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

3 件のコメント:

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

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

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

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

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

      削除