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からの接続およびメッセージを確認できます。