Android実機を用いたSocket通信サーバーの構築(WIFIおよびADB接続)

Androidアプリケーションの開発実践において、エミュレータの環境設定が困難な場合や、実機特有の挙動を確認したい場合、物理デバイスを利用した開発は非常に有効です。本記事では、Android端末をサーバー、PCをクライアントとしてSocket通信を行う実装手法について解説します。将来的には、クライアント側をWindows CEやAndroid搭載の車載システムへ置き換えることを想定したアーキテクチャとなっています。

マルチスレッド環境でのAndroid開発において、ワーカースレッドからメインスレッド(UIスレッド)へデータを通知するには、Handlerクラスを用いたメッセージング機構が不可欠です。

Android端末(サーバー側)の実装

以下に、WIFI経由で通信を受け付けるAndroidアプリケーションのコード例を示します。レイアウトXML(TextViewの配置など)は省略しますが、ログ出力とUI更新の両方を処理する構成となっています。

package com.example.networkserver;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerActivity extends AppCompatActivity {

    private static final int SERVER_PORT = 8899;
    private static final int MSG_CONNECTED = 100;
    private static final int MSG_DATA_RECEIVED = 101;

    private ServerSocket serverSocket;
    private Socket clientConnection;
    private TextView statusView;
    private TextView dataView;
    private volatile boolean isRunning = true;

    // UIスレッドでの処理を行うハンドラー
    private final Handler uiHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_CONNECTED) {
                String clientIp = (String) msg.obj;
                statusView.setText("接続状態: 接続済み (" + clientIp + ")");
                showToast("クライアントが接続されました: " + clientIp);
            } else if (msg.what == MSG_DATA_RECEIVED) {
                String content = (String) msg.obj;
                dataView.setText("受信データ: " + content);
                Log.d("ServerLog", "Received: " + content);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        statusView = findViewById(R.id.status_text);
        dataView = findViewById(R.id.data_text);

        statusView.setText("接続状態: 待機中...");

        // サーバー待機スレッドの起動
        new Thread(new ServerListener()).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isRunning = false;
        try {
            if (serverSocket != null && !serverSocket.isClosed()) {
                serverSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }

    // クライアントの接続を受け入れるスレッド
    private class ServerListener implements Runnable {
        @Override
        public void run() {
            try {
                serverSocket = new ServerSocket(SERVER_PORT);
                while (isRunning) {
                    // クライアントからの接続待機(ブロッキング)
                    clientConnection = serverSocket.accept();
                    
                    String ip = clientConnection.getInetAddress().getHostAddress();
                    
                    // 接続通知をUIスレッドへ送信
                    Message connectMsg = uiHandler.obtainMessage(MSG_CONNECTED, ip);
                    uiHandler.sendMessage(connectMsg);

                    // 通信用スレッドを起動
                    new Thread(new DataReceiver(clientConnection)).start();
                }
            } catch (IOException e) {
                Log.e("ServerLog", "Server Error: " + e.getMessage());
            }
        }
    }

    // データ受信用スレッド
    private class DataReceiver implements Runnable {
        private Socket socket;
        private InputStream input;

        DataReceiver(Socket s) {
            this.socket = s;
            try {
                this.input = s.getInputStream();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    // 受信データをUIスレッドへ送信
                    Message dataMsg = uiHandler.obtainMessage(MSG_DATA_RECEIVED, line);
                    uiHandler.sendMessage(dataMsg);
                }
            } catch (IOException e) {
                Log.e("ServerLog", "Connection Lost: " + e.getMessage());
            }
        }
    }
}

1. WIFI経由での通信テスト

Android端末とPCが同一のローカルネットワーク内にある状態で、上記アプリを起動します。PC側から「SocketTool」などのネットワーク検証ツールを使用して、Android端末のIPアドレス(例: 192.168.1.5)とポート番号(8899)へTCP接続を行います。接続が確立されると、Android端末の画面上にクライアントのIPアドレスが表示され、その後PCから送信された文字列が受信データとして表示されます。

2. USB(ADB)経由での通信テスト

USBケーブル経由で通信を行う場合、ADBのポートフォワーディング機能を使用します。これにより、PCのローカルポートへのアクセスを、Android端末の特定ポートへ転送できます。PC側で以下のようなJavaアプリケーションを作成し、実行します。

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class PcClient {
    public static void main(String[] args) {
        // ポートフォワーディングの設定: PCの6001番をデバイスの8899番へ転送
        String portForwardCmd = "adb forward tcp:6001 tcp:8899";
        
        try {
            Runtime.getRuntime().exec(portForwardCmd);
            System.out.println("ADB Port forwarding executed.");
            Thread.sleep(1000); // 設定反映のための待機
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        Socket socket = null;
        OutputStream outStream = null;

        try {
            // 転送設定されたローカルホストのポートへ接続
            InetAddress serverAddr = InetAddress.getByName("127.0.0.1");
            socket = new Socket(serverAddr, 6001);

            System.out.println("Connected to Android Device via ADB.");

            // データの送信
            String message = "Hello from PC via ADB";
            outStream = socket.getOutputStream();
            outStream.write(message.getBytes("UTF-8"));
            outStream.flush();
            
            System.out.println("Message sent: " + message);
            
            Thread.sleep(5000); // 確認のための待機

        } catch (UnknownHostException e) {
            System.err.println("Host error: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("IO error: " + e.getMessage());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

このJavaアプリケーションを実行すると、コマンドプロンプト等を通じてADBポート転送が行われ、その後ローカルホストへ向けてSocket接続が試行られます。Android端末側のアプリケーションでは、PCからの接続およびメッセージを確認できます。

タグ: Android socket TCP/IP ADB Java

7月13日 17:45 投稿