Android - 如何 运行 长寿命后台线程中的套接字

Android - how to run a socket in a long lives background thread

我正在构建一个连接 android 客户端和 java 服务器(运行ning 在我的电脑上)的简单聊天。用户可以通过 android 应用程序和桌面服务器发送和接收消息 to/from。
我现在正在处理如何在与 UI Thread 不同的线程中 运行 客户端套接字的问题。

我看到了使用 AsyncTask 的解决方案,但由于用户可能会在 较长的连续时间内使用该应用程序进行交流 AsyncTask 看起来是个糟糕的方法.

AsyncTasks should ideally be used for short operations (a few seconds at the most.) API

因为我需要客户端套接字始终侦听来自桌面服务器的消息,我想创建新的Thread接收Runnable实现class.

我的问题
1. 在"thread mechanism"中放置客户端套接字行(Thread,IntentService)?

Socket client = new Socket(host, port);
InputStreamReader inputStreamReader = new InputStreamReader(client.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
while ((messageFromServer = bufferedReader.readLine()) != null) { //... }

2。客户端套接字(运行ning 来自与 main thread 不同的线程)如何 post messageFromServerTextView

  1. 当用户输入文本并单击按钮时,我如何将用户消息从应用程序发送到服务器(当然使用客户端套接字)?

谢谢!

我建议您查看 Android 后台服务文档。我个人会使用 IntentService,因为它是 Android.

中的一个成熟模式

http://developer.android.com/training/run-background-service/index.html

我创建了一个类似的应用程序,并且我使用了一个 运行 在后台运行的服务。

  1. 我已经从 IntentService 类中复制了代码并更新了 handleMessage(Message msg) 方法并删除了 stopSelf(msg.arg1); 行。通过这种方式,您可以在后台获得 运行 的服务。之后我使用线程进行连接。
  2. 这里有两个选择。将数据存储到数据库中,GUI 会自行刷新。或者使用 LocalBroadcastManager.
  3. 在这里你也可以将数据存储到数据库中或以特殊意图启动服务。

这是我的实现。我希望你能理解代码。

public class KeepAliveService extends Service {

/**
 * The source of the log message.
 */
private static final String TAG = "KeepAliveService";

private static final long INTERVAL_KEEP_ALIVE = 1000 * 60 * 4;

private static final long INTERVAL_INITIAL_RETRY = 1000 * 10;

private static final long INTERVAL_MAXIMUM_RETRY = 1000 * 60 * 2;

private ConnectivityManager mConnMan;

protected NotificationManager mNotifMan;

protected AlarmManager mAlarmManager;

private boolean mStarted;

private boolean mLoggedIn;

protected static ConnectionThread mConnection;

protected static SharedPreferences mPrefs;

private final int maxSize = 212000;

private Handler mHandler;

private volatile Looper mServiceLooper;

private volatile ServiceHandler mServiceHandler;

private final class ServiceHandler extends Handler {
    public ServiceHandler(final Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(final Message msg) {
        onHandleIntent((Intent) msg.obj);
    }
}

public static void actionStart(final Context context) {
    context.startService(SystemHelper.createExplicitFromImplicitIntent(context, new Intent(IntentActions.KEEP_ALIVE_SERVICE_START)));
}

public static void actionStop(final Context context) {
    context.startService(SystemHelper.createExplicitFromImplicitIntent(context, new Intent(IntentActions.KEEP_ALIVE_SERVICE_STOP)));
}

public static void actionPing(final Context context) {
    context.startService(SystemHelper.createExplicitFromImplicitIntent(context, new Intent(IntentActions.KEEP_ALIVE_SERVICE_PING_SERVER)));
}

@Override
public void onCreate() {
    Log.i(TAG, "onCreate called.");
    super.onCreate();

    mPrefs = getSharedPreferences("KeepAliveService", MODE_PRIVATE);

    mConnMan = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);

    mNotifMan = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

    mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

    mHandler = new Handler();

    final HandlerThread thread = new HandlerThread("IntentService[KeepAliveService]");
    thread.start();

    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);

    // If our process was reaped by the system for any reason we need to
    // restore our state with merely a
    // call to onCreate.
    // We record the last "started" value and restore it here if necessary.
    handleCrashedService();
}

@Override
public void onDestroy() {
    Log.i(TAG, "Service destroyed (started=" + mStarted + ")");
    if (mStarted) {
        stop();
    }
    mServiceLooper.quit();
}

private void handleCrashedService() {
    Log.i(TAG, "handleCrashedService called.");
    if (isStarted()) {
        // We probably didn't get a chance to clean up gracefully, so do it now.
        stopKeepAlives();

        // Formally start and attempt connection.
        start();
    }
}

/**
 * Returns the last known value saved in the database.
 */
private boolean isStarted() {
    return mStarted;
}

private void setStarted(final boolean started) {
    Log.i(TAG, "setStarted called with value: " + started);
    mStarted = started;
}

protected void setLoggedIn(final boolean value) {
    Log.i(TAG, "setLoggedIn called with value: " + value);
    mLoggedIn = value;
}

protected boolean isLoggedIn() {
    return mLoggedIn;
}

public static boolean isConnected() {
    return mConnection != null;
}

@Override
public void onStart(final Intent intent, final int startId) {
    final Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    mServiceHandler.sendMessage(msg);
}

@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
    Log.i(TAG, "Service started with intent : " + intent);

    onStart(intent, startId);

    return START_NOT_STICKY;
}

private void onHandleIntent(final Intent intent) {
    if (IntentActions.KEEP_ALIVE_SERVICE_STOP.equals(intent.getAction())) {
        stop();

        stopSelf();
    } else if (IntentActions.KEEP_ALIVE_SERVICE_START.equals(intent.getAction())) {
        start();
    } else if (IntentActions.KEEP_ALIVE_SERVICE_PING_SERVER.equals(intent.getAction())) {
        keepAlive(false);
    }
}

@Override
public IBinder onBind(final Intent intent) {
    return null;
}

private synchronized void start() {
    if (mStarted) {
        Log.w(TAG, "Attempt to start connection that is already active");
        setStarted(true);
        return;
    }

    try {
        registerReceiver(mConnectivityChanged, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
    } catch (final Exception e) {
        Log.e(TAG, "Exception occurred while trying to register the receiver.", e);
    }

    if (mConnection == null) {
        Log.i(TAG, "Connecting...");
        mConnection = new ConnectionThread(Config.PLUGIN_BASE_HOST, Config.PLUGIN_BASE_PORT);
        mConnection.start();
    }
}

private synchronized void stop() {
    if (mConnection != null) {
        mConnection.abort(true);
        mConnection = null;
    }

    setStarted(false);

    try {
        unregisterReceiver(mConnectivityChanged);
    } catch (final Exception e) {
        Log.e(TAG, "Exception occurred while trying to unregister the receiver.", e);
    }
    cancelReconnect();
}

/**
 * Sends the keep-alive message if the service is started and we have a
 * connection with it.
 */
private synchronized void keepAlive(final Boolean forced) {
    try {
        if (mStarted && isConnected() && isLoggedIn()) {
            mConnection.sendKeepAlive(forced);
        }
    } catch (final IOException e) {
        Log.w(TAG, "Error occurred while sending the keep alive message.", e);
    } catch (final JSONException e) {
        Log.w(TAG, "JSON error occurred while sending the keep alive message.", e);
    }
}


/**
 * Uses the {@link android.app.AlarmManager} to start the keep alive service in every {@value #INTERVAL_KEEP_ALIVE} milliseconds.
 */
private void startKeepAlives() {
    final PendingIntent pi = PendingIntent.getService(this, 0, new Intent(IntentActions.KEEP_ALIVE_SERVICE_PING_SERVER), PendingIntent.FLAG_UPDATE_CURRENT);
    mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + INTERVAL_KEEP_ALIVE, INTERVAL_KEEP_ALIVE, pi);
}

/**
 * Removes the repeating alarm which was started by the {@link #startKeepAlives()} function.
 */
private void stopKeepAlives() {
    final PendingIntent pi = PendingIntent.getService(this, 0, new Intent(IntentActions.KEEP_ALIVE_SERVICE_PING_SERVER), PendingIntent.FLAG_UPDATE_CURRENT);
    mAlarmManager.cancel(pi);
}

public void scheduleReconnect(final long startTime) {
    long interval = mPrefs.getLong("retryInterval", INTERVAL_INITIAL_RETRY);

    final long now = System.currentTimeMillis();
    final long elapsed = now - startTime;

    if (elapsed < interval) {
        interval = Math.min(interval * 4, INTERVAL_MAXIMUM_RETRY);
    } else {
        interval = INTERVAL_INITIAL_RETRY;
    }

    Log.i(TAG, "Rescheduling connection in " + interval + "ms.");

    mPrefs.edit().putLong("retryInterval", interval).apply();

    final PendingIntent pi = PendingIntent.getService(this, 0, new Intent(IntentActions.KEEP_ALIVE_SERVICE_RECONNECT), PendingIntent.FLAG_UPDATE_CURRENT);
    mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + interval, pi);
}

public void cancelReconnect() {
    final PendingIntent pi = PendingIntent.getService(this, 0, new Intent(IntentActions.KEEP_ALIVE_SERVICE_RECONNECT), PendingIntent.FLAG_UPDATE_CURRENT);
    mAlarmManager.cancel(pi);
}

private synchronized void reconnectIfNecessary() {
    if (mStarted && !isConnected()) {
        Log.i(TAG, "Reconnecting...");

        mConnection = new ConnectionThread(Config.PLUGIN_BASE_HOST, Config.PLUGIN_BASE_PORT);
        mConnection.start();
    }
}

private final BroadcastReceiver mConnectivityChanged = new BroadcastReceiver() {
    @Override
    public void onReceive(final Context context, final Intent intent) {
        final NetworkInfo info = mConnMan.getActiveNetworkInfo(); //  (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);

        final boolean hasConnectivity = info != null && info.isConnected();

        Log.i(TAG, "Connecting changed: connected=" + hasConnectivity);

        if (hasConnectivity) {
            reconnectIfNecessary();
        } else if (mConnection != null) {
            mConnection.abort(false);
            mConnection = null;
        }
    }
};

protected class ConnectionThread extends Thread {
    private final Socket mSocket;

    private final String mHost;

    private final int mPort;

    private volatile boolean mAbort = false;

    public ConnectionThread(final String host, final int port) {
        mHost = host;
        mPort = port;
        mSocket = new Socket();
    }

    /**
     * Returns whether we have an active internet connection or not.
     *
     * @return <code>true</code> if there is an active internet connection.
     * <code>false</code> otherwise.
     */
    private boolean isNetworkAvailable() {
        final NetworkInfo info = mConnMan.getActiveNetworkInfo();
        return info != null && info.isConnected();
    }

    @Override
    public void run() {
        final Socket s = mSocket;

        final long startTime = System.currentTimeMillis();

        try {
            // Now we can say that the service is started.
            setStarted(true);

            // Connect to server.
            s.connect(new InetSocketAddress(mHost, mPort), 20000);

            Log.i(TAG, "Connection established to " + s.getInetAddress() + ":" + mPort);

            // Start keep alive alarm.
            startKeepAlives();

            final DataOutputStream dos = new DataOutputStream(s.getOutputStream());
            final BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"));

            // Send the login data.
            final JSONObject login = new JSONObject();

            // Send the login message.
            dos.write((login.toString() + "\r\n").getBytes());

            // Wait until we receive something from the server.
            String receivedMessage;
            while ((receivedMessage = in.readLine()) != null) {
                Log.i(TAG, "Received data: " + receivedMessage);
                processMessagesFromServer(dos, receivedMessage);
            }

            if (!mAbort) {
                Log.i(TAG, "Server closed connection unexpectedly.");
            }
        } catch (final IOException e) {
            Log.e(TAG, "Unexpected I/O error.", e);
        } catch (final Exception e) {
            Log.e(TAG, "Exception occurred.", e);
        } finally {
            setLoggedIn(false);
            stopKeepAlives();

            if (mAbort) {
                Log.i(TAG, "Connection aborted, shutting down.");
            } else {
                try {
                    s.close();
                } catch (final IOException e) {
                    // Do nothing.
                }

                synchronized (KeepAliveService.this) {
                    mConnection = null;
                }

                if (isNetworkAvailable()) {
                    scheduleReconnect(startTime);
                }
            }
        }
    }

    /**
     * Sends the PING word to the server.
     *
     * @throws java.io.IOException    if an error occurs while writing to this stream.
     * @throws org.json.JSONException
     */
    public void sendKeepAlive(final Boolean forced) throws IOException, JSONException {
        final JSONObject ping = new JSONObject();

        final Socket s = mSocket;
        s.getOutputStream().write((ping.toString() + "\r\n").getBytes());
    }

    /**
     * Aborts the connection with the server.
     */
    public void abort(boolean manual) {
        mAbort = manual;

        try {
            // Close the output stream.
            mSocket.shutdownOutput();
        } catch (final IOException e) {
            // Do nothing.
        }

        try {
            // Close the input stream.
            mSocket.shutdownInput();
        } catch (final IOException e) {
            // Do nothing.
        }

        try {
            // Close the socket.
            mSocket.close();
        } catch (final IOException e) {
            // Do nothing.
        }

        while (true) {
            try {
                join();
                break;
            } catch (final InterruptedException e) {
                // Do nothing.
            }
        }
    }
}

public void processMessagesFromServer(final DataOutputStream dos, final String receivedMessage) throws IOException {
}

}

可以调用KeepAliveService.actionStart()启动服务,也可以自定义函数

请注意,仅当您致电 KeepAliveService.actionStop() 时,服务才会停止。否则它将永远运行。如果你打电话KeepAliveService.actionSendMessage(String message) 然后 intent 将传递给服务,您可以轻松处理它。

编辑:

SystemHelper class 只是一个包含静态方法的实用程序 class。

public class SystemHelper {

    /**
     * Android Lollipop, API 21 introduced a new problem when trying to invoke implicit intent,
     * "java.lang.IllegalArgumentException: Service Intent must be explicit"
     *
     * If you are using an implicit intent, and know only 1 target would answer this intent,
     * This method will help you turn the implicit intent into the explicit form.
     *
     * Inspired from SO answer: 
     * @param context the application context
     * @param implicitIntent - The original implicit intent
     * @return Explicit Intent created from the implicit original intent
     */
    public static Intent createExplicitFromImplicitIntent(Context context, Intent implicitIntent) {
        Log.i(TAG, "createExplicitFromImplicitIntent ... called with intent: " + implicitIntent);
        // Retrieve all services that can match the given intent
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);

        // Make sure only one match was found
        if (resolveInfo == null || resolveInfo.size() != 1) {
            Log.i(TAG, "createExplicitFromImplicitIntent ... resolveInfo is null or there are more than one element.");
            return null;
        }

        // Get component info and create ComponentName
        ResolveInfo serviceInfo = resolveInfo.get(0);
        String packageName = serviceInfo.serviceInfo.packageName;
        String className = serviceInfo.serviceInfo.name;
        ComponentName component = new ComponentName(packageName, className);

        Log.i(TAG, "createExplicitFromImplicitIntent ... found package name:" + packageName + ", class name: " + className + ".");

        // Create a new intent. Use the old one for extras and such reuse
        Intent explicitIntent = new Intent(implicitIntent);

        // Set the component to be explicit
        explicitIntent.setComponent(component);

        return explicitIntent;
    }
}

Config class.

public class Config {

    public static final String PACKAGE_NAME = "com.yourapp.package";
    public static final String PLUGIN_BASE_HOST = "test.yoursite.com";
    public static final int PLUGIN_BASE_PORT = 10000;
}

还有 IntentActions class.

public class IntentActions {

    public static final String KEEP_ALIVE_SERVICE_START = Config.PACKAGE_NAME + ".intent.action.KEEP_ALIVE_SERVICE_START";
    public static final String KEEP_ALIVE_SERVICE_STOP = Config.PACKAGE_NAME + ".intent.action.KEEP_ALIVE_SERVICE_STOP";
    public static final String KEEP_ALIVE_SERVICE_PING_SERVER = Config.PACKAGE_NAME + ".intent.action.KEEP_ALIVE_SERVICE_PING_SERVER";
}

在 AndroidManifest 文件中,服务定义如下:

<service android:name="com.yourapp.package.services.KeepAliveService"
         android:exported="false">
    <intent-filter>
        <action android:name="com.yourapp.package.intent.action.KEEP_ALIVE_SERVICE_START" />
        <action android:name="com.yourapp.package.intent.action.KEEP_ALIVE_SERVICE_STOP" />
        <action android:name="com.yourapp.package.intent.action.KEEP_ALIVE_SERVICE_PING_SERVER" />
    </intent-filter>
</service>