Android中的IPC方式 - ContentProvider与Socket

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_20967339/article/details/82704696

本节重点
使用ContentProvider
1. ContentProvider是四大组件之其底层实现和Messenger一样是BinderContentProvider天生就是用来进程间通信,只需要实现一个自定义或者系统预设置的ContentProvider,通过ContentResolver的query、update、insert和delete方法即可。
2. 创建ContentProvider,只需继承ContentProvider实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。除了 onCreate
由系统回调并运行在主线程,其他五个方法都由外界调用并运行在Binder线程池中。
使用Sock
Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。
服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。

DEMO下载

IPC方式之ContentProvider的DEMO下载
IPC方式之Socket的Demo下载

使用ContentProvider

ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和Messenger一样,ContentProvider的底层实现同样也是Binder,由此可见,Binder在Android系统中是何等的重要。虽然ContentProvider的底层实现是Binder,但是它的使用过程要比AIDL简单许多,这是因为系统已经为我们做了封装,使得我们无须关心底层细节即可轻松实现IPC。ContentProvider虽然使用起来很简单,包括自己创建一个ContentProvider也不是什么难事,尽管如此,它的细节还是相当多,比如CRUD操作、防止SQL注入和权限控制等。由于章节主题限制,在本节中,笔者暂时不对ContentProvider的使用细节以及工作机制进行详细分析,而是为读者介绍采用ContentProvider进行跨进程通信的主要流程,至于使用细节和内部工作机制会在后续章节进行详细分析。
系统预置了许多ContentProvider,比如通讯录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。在本节中,我们来实现一个自定义的ContentProvider,并演示如何在其他应用中获取ContentProvider中的数据从而实现进程间通信这一目的。首先,我们创建一个ContentProvider,名字就叫BookProvider。创建一个自定义的ContentProvider很简单,只需要继承ContentProvider类并实现六个抽象方法即可:onCreate、query、update、insert、delete和getType。这六个抽象方法都很好理解,onCreate代表ContentProvider的创建,一般来说我们需要做一些初始化工作;getType用来返回一个Uri请求所对应的MIME类型(媒体类型),比如图片、视频等,这个媒体类型还是有点复杂的,如果我们的应用不关注这个选项,可以直接在这个方法中返回null或者“/”;剩下的四个方法对应于CRUD操作,即实现对数据表的增删改查功能。根据Binder的工作原理,我们知道这六个方法均运行在ContentProvider的进程中,除了onCreate由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在Binder线程池中,这一点在接下来的例子中可以再次证明。
ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一条记录,而列对应一条记录中的一个字段,这点和数据库很类似。除了表格的形式,ContentProvider还支持文件数据,比如图片、视频等。文件数据和表格数据的结构不同,因此处理这类数据时可以在ContentProvider中返回文件的句柄给外界从而让文件来访问ContentProvider中的文件信息。Android系统所提供的MediaStore功能就是文件类型的ContentProvider,详细实现可以参考MediaStore。另外,虽然ContentProvider的底层数据看起来很像一个SQLite数据库,
但是ContentProvider对底层的数据存储方式没有任何要求,我们既可以使用SQLite数据库,也可以使用普通的文件,甚至可以采用内存中的一个对象来进行数据的存储,这一点在后续的章节中会再次介绍,所以这里不再深入了。
下面看一个最简单的示例,它演示了ContentProvider的工作工程。首先创建一个BookProvider类,它继承自ContentProvider并实现了ContentProvider的六个必须需要实现的抽象方法。在下面的代码中,我们什么都没干,尽管如此,这个BookProvider也是可以工作的,只是它无法向外界提供有效的数据而已。

public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
@Override
public boolean onCreate() {Log.d(TAG,"onCreate,current thread:" + Thread.currentThread().getName());
return false;
}
@Override
public Cursor query(Uri uri,String[] projection,String selection,
String[] selectionArgs,String sortOrder) {
Log.d(TAG,"query,current thread:" + Thread.currentThread().
getName());
return null;
}
@Override
public String getType(Uri uri) {
Log.d(TAG,"getType");
return null;
}
@Override
public Uri insert(Uri uri,ContentValues values) {
Log.d(TAG,"insert");
return null;
}
@Override
public int delete(Uri uri,String selection,String[] selectionArgs) {
Log.d(TAG,"delete");
return 0;
}
@Override
public int update(Uri uri,ContentValues values,String selection,
String[] selectionArgs) {
Log.d(TAG,"update");
return 0;
}
}

接着我们需要注册这个BookProvider,如下所示。其中android:authorities是ContentProvider的唯一标识,通过这个属性外部应用就可以访问我们的BookProvider,因此,android:authorities必须是唯一的,这里建议读者在命名的时候加上包名前缀。为了演示进程间通信,我们让BookProvider运行在独立的进程中并给它添加了权限,这样外界应用如果想访问BookProvider,就必须声明“com.ryg.PROVIDER”这个权限。ContentProvider的权限还可以细分为读权限和写权限,分别对应android:readPermission和
android:writePermission属性,如果分别声明了读权限和写权限,那么外界应用也必须依次声明相应的权限才可以进行读/写操作,否则外界应用会异常终止。关于权限这一块,请读者自行查阅相关资料,本章不进行详细介绍

 <permission
        android:name="com.ryg.PROVIDER"
        android:protectionLevel="normal" />
   <provider
            android:name="com.nextvpu.myapplication.BookProvider"
            android:authorities="com.nextvpu.myapplication"
            android:permission="com.ryg.PROVIDER"
            android:process=":provider" >
        </provider>

注册了ContentProvider以后,我们就可以在外部应用中访问它了。为了方便演示,这里仍然选择在同一个应用的其他进程中去访问这个BookProvider,至于在单独的应用中去访问这个BookProvider,和同一个应用中访问的效果是一样的,读者可以自行试一下(注意要声明对应权限)。

public class ProviderActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_provider);
Uri uri = Uri.parse("content://com.ryg.chapter_2.book.provider");
getContentResolver().query(uri,null,null,null,null);
getContentResolver().query(uri,null,null,null,null);
getContentResolver().query(uri,null,null,null,null);
}
}

在上面的代码中,我们通过ContentResolver对象的query方法去查询BookProvider中的数据,其中“content://com.nextvpu.myapplication/book”唯一标识了BookProvider,而这个标识正是我们前面为BookProvider的android:authorities属性所指定的值。我们运行后看一下
log。从下面log可以看出,BookProvider中的query方法被调用了三次,并且这三次调用不在同一个线程中。可以看出,它们运行在一个Binder线程中,前面提到update、insert和delete方法同样也运行在Binder线程中。另外,onCreate运行在main线程中,也就是UI线程,所以我们不能在onCreate中做耗时操作.
到这里,整个ContentProvider的流程我们已经跑通了,虽然ContentProvider中没有返回任何数据。接下来,在上面的基础上,我们继续完善BookProvider,从而使其能够对外部应用提供数据。继续本章提到的那个例子,现在我们要提供一个BookProvider,外部应
用可以通过BookProvider来访问图书信息,为了更好地演示ContentProvider的使用,用户还可以通过BookProvider访问到用户信息。为了完成上述功能,我们需要一个数据库来管理图书和用户信息,这个数据库不难实现,代码如下:

package com.nextvpu.myapplication;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DbOpenHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "book_provider.db";
    public static final String BOOK_TABLE_NAME = "book";
    public static final String USER_TABLE_NAME = "user";

    private static final int DB_VERSION = 3;

    private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS " + BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";
    private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT," + "sex INT)";

    public DbOpenHelper(Context context) {
        super(context,DB_NAME,null,DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK_TABLE);
        db.execSQL(CREATE_USER_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

上述代码是一个最简单的数据库的实现,我们借助SQLiteOpenHelper来管理数据库的创建、升级和降级。下面我们就要通过BookProvider向外界提供上述数据库中的信息了。我们知道,ContentProvider通过Uri来区分外界要访问的的数据集合,在本例中支持外界对BookProvider中的book表和user表进行访问,为了知道外界要访问的是哪个表,我们需要为它们定义单独的Uri和Uri_Code,并将Uri和对应的Uri_Code相关联,我们可以使用UriMatcher的addURI方法将Uri和Uri_Code关联到一起。这样,当外界请求访问BookProvider时,我们就可以根据请求的Uri来得到Uri_Code,有了Uri_Code我们就可以知道外界想要访问哪个表,然后就可以进行相应的数据操作了,具体代码如下所示。

public class BookProvider extends ContentProvider{

    private static final String TAG = "BookProvider";

    public static final String AUTHORITY = "com.nextvpu.myapplication";

    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/book");
    public static final Uri USER_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/user");

    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
    }
    ......

从上面代码可以看出,我们分别为book表和user表指定了Uri,分别
为“content://com.nextvpu.myapplication/book”和“content://com.nextvpu.myapplication/user”,这两个Uri所关联的Uri_Code分别为0和1。这个关联过程是通过下面的语句来完成的:

static {
sUriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
    }

将Uri和Uri_Code管理以后,我们就可以通过如下方式来获取外界所要访问的数据源,根据Uri先取出Uri_Code,根据Uri_Code再得到数据表的名称,知道了外界要访问的表,接下来就可以响应外界的增删改查请求了。

private String getTableName(Uri uri){
        String tableName = null;
        switch (sUriMatcher.match(uri)){
            case BOOK_URI_CODE:
                tableName = DbOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_URI_CODE:
                tableName = DbOpenHelper.USER_TABLE_NAME;
                break;
                default:break;
        }
        return tableName;
    }

接着,我们就可以实现query、update、insert、delete方法了。如下是query方法的实现,首先我们要从Uri中取出外界要访问的表的名称,然后根据外界传递的查询参数就可以进行数据库的查询操作了,这个过程比较简单。

@Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Log.e("xyz", "query, current thread:" + Thread.currentThread().getName());
      String table = getTableName(uri);
      if (table == null){
          throw  new IllegalArgumentException("Unsupported URI:" + uri);
      }

        return mDb.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

另外三个方法的实现思想和query是类似的,只有一点不同,那就是update、insert和delete方法会引起数据源的改变,这个时候我们需要通过ContentResolver的notifyChange方法来通知外界当前ContentProvider中的数据已经发生改变。要观察一个ContentProvider中的数据改变情况,可以通过ContentResolver的registerContentObserver方法来注册观察者,通过unregisterContentObserver方法来解除观察者。对于这三个方法,这里不再详细解释了,BookProvider的完整代码如下:

package com.nextvpu.myapplication;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

public class BookProvider extends ContentProvider{

    private static final String TAG = "BookProvider";

    public static final String AUTHORITY = "com.nextvpu.myapplication";

    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/book");
    public static final Uri USER_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/user");

    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
    }

    private Context mContext;
    private SQLiteDatabase mDb;

    @Override
    public boolean onCreate() {
        Log.e("xyz","onCreate, current thread:"
                + Thread.currentThread().getName());
        mContext = getContext();
        initProviderData();
        return true;
    }

    private void initProviderData() {
        mDb = new DbOpenHelper(mContext).getWritableDatabase();
        mDb.execSQL("delete from "+DbOpenHelper.BOOK_TABLE_NAME);
        mDb.execSQL("delete from "+DbOpenHelper.USER_TABLE_NAME);
        mDb.execSQL("insert into book values(3,'Android')");
        mDb.execSQL("insert into book values(4,'Ios');");
        mDb.execSQL("insert into book values(5,'Html5');");
        mDb.execSQL("insert into user values(1,'jake',1);");
        mDb.execSQL("insert into user values(2,'jasmine',0);");

    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Log.e("xyz", "query, current thread:" + Thread.currentThread().getName());
      String table = getTableName(uri);
      if (table == null){
          throw  new IllegalArgumentException("Unsupported URI:" + uri);
      }

        return mDb.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {

        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        Log.e("xyz", "insert");
        String table = getTableName(uri);
        if(table == null){
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        mDb.insert(table,null,values);
        mContext.getContentResolver().notifyChange(uri,null);
        return uri;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.e("xyz", "delete");
        String table = getTableName(uri);
        if(table == null){
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int delete = mDb.delete(table, selection, selectionArgs);
        mContext.getContentResolver().notifyChange(uri,null);
        return delete;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.e("xyz", "update");
        String table = getTableName(uri);
        if (table == null){
            throw new IllegalArgumentException("Unsupported URI: "+uri);
        }
        int update = mDb.update(table, values, selection, selectionArgs);
        if (update>0){
            getContext().getContentResolver().notifyChange(uri,null);
        }
        return update;
    }

    private String getTableName(Uri uri){
        String tableName = null;
        switch (sUriMatcher.match(uri)){
            case BOOK_URI_CODE:
                tableName = DbOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_URI_CODE:
                tableName = DbOpenHelper.USER_TABLE_NAME;
                break;
                default:break;
        }
        return tableName;
    }
}

需要注意的是,query、update、insert、delete四大方法是存在多线程并发访问的,因此方法内部要做好线程同步。在本例中,由于采用的是SQLite并且只有一个SQLiteDatabase的连接,所以可以正确应对多线程的情况。具体原因是SQLiteDatabase内部对数据库的操作是有同步处理的,但是如果通过多个SQLiteDatabase对象来操作数据库
就无法保证线程同步,因为SQLiteDatabase对象之间无法进行线程同步。如果ContentProvider的底层数据集是一块内存的话,比如是List,在这种情况下同List的遍历、插入、删除操作就需要进行线程同步,否则就会引起并发错误,这点是尤其需要注意的。到这里BookProvider已经实现完成了,接着我们在外部访问一下它,看看是否能够正常工作。

package com.nextvpu.myapplication;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

        Uri bookUri = Uri.parse("content://com.nextvpu.myapplication/book") ;
        ContentValues values = new ContentValues();
        values.put("_id",6);
        values.put("name","程序设计的艺术");
        getContentResolver().insert(bookUri,values);
        Cursor bookCursor = getContentResolver().query(bookUri,new String[]{"_id","name"},null,null,null);
        while (bookCursor.moveToNext()){
            Book book = new Book();
            book.bookId = bookCursor.getInt(0);
            book.bookName = bookCursor.getString(1);
            Log.e("xyz"," query book: "+book.toString());
        }
        bookCursor.close();

        Uri userUri = Uri.parse("content://com.nextvpu.myapplication/user") ;
        Cursor userCursor = getContentResolver().query(userUri,new String[]{"_id","name","sex"},null,null,null);
        while (userCursor.moveToNext()){
            User user = new User();
           user.userId = userCursor.getInt(0);
           user.userName = userCursor.getString(1);
           user.isMale = userCursor.getInt(2) == 1;
           Log.e("xyz"," query user: "+user.toString());
        }
        userCursor.close();

    }
}

默认情况下,BookProvider的数据库中有三本书和两个用户,在上面的代码中,我们首先添加一本书:“程序设计的艺术”。接着查询所有的图书,这个时候应该查询出四本书,因为我们刚刚添加了一本。然后查询所有的用户,这个时候应该查询出两个用户。是不是这样呢?我们运行一下程序,看一下log

09-14 14:46:17.335 11725-11725/? E/xyz: onCreate, current thread:main
09-14 14:46:17.426 11725-11747/? E/xyz: insert
09-14 14:46:17.440 11725-11747/? E/xyz: query, current thread:Binder:11725_3
09-14 14:46:17.447 11610-11610/com.nextvpu.myapplication E/xyz:  query book: [bookId:3, bookName:Android]
     query book: [bookId:4, bookName:Ios]
     query book: [bookId:5, bookName:Html5]
     query book: [bookId:6, bookName:程序设计的艺术]
09-14 14:46:17.451 11725-11747/? E/xyz: query, current thread:Binder:11725_3
09-14 14:46:17.455 11610-11610/com.nextvpu.myapplication E/xyz:  query user: User:{userId:1, userName:jake, isMale:true}, with child:{null}
09-14 14:46:17.456 11610-11610/com.nextvpu.myapplication E/xyz:  query user: User:{userId:2, userName:jasmine, isMale:false}, with child:{null}

从上述log可以看到,我们的确查询到了4本书和2个用户,这说明BookProvider已经能够正确地处理外部的请求了,读者可以自行验证一下update和delete操作,这里就不再验证了。同时,由于ProviderActivity和BookProvider运行在两个不同的进程中,因此,这也构成了进程间的通信。ContentProvider除了支持对数据源的增删改查这四个操作,还支持自定义调用,这个过程是通过ContentResolve的Call方法和ContentProvider的Call方法来完成的。关于使用ContentProvider来进行IPC就介绍到这里,ContentProvider本身还有一些细节这里并没有介绍,读者可以自行了解,本章侧重的是各种进程间通信的方法以及它们的区别,因此针对某种特定的方法可能不会介绍得面面俱到。另外,ContentProvider在后续章节还会有进一步的讲解,主要包括细节问题和工作原理,读者可以阅读后面的相应章节。

使用Socket

在本节中,我们通过Socket来实现进程间的通信。Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。在性能上,UDP具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。关于TCP和UDP的介绍就这么多,更详细的资料请查看相关网络资料。接下来我们演示一个跨进程的聊天程序,两个进程可以通过Socket来实现信息的传输,Socket本身可以支持传输任意字节流,这里为了简单起见,仅仅传输文本信息,很显然,这是一种IPC方式。
使用Socket来进行通信,有两点需要注意,首先需要声明权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

其次要注意不能在主线程中访问网络,因为这会导致我们的程序无法在Android 4.0及
其以上的设备中运行,会抛出如下异常:android.os.NetworkOnMainThreadException。而且进行网络操作很可能是耗时的,如果放在主线程中会影响程序的响应效率,从这方面来说,也不应该在主线程中访问网络。下面就开始设计我们的聊天室程序了,比较简单,首先在远程Service建立一个TCP服务,然后在主界面中连接TCP服务,连接上了以后,就可以给服务端发消息。对于我们发送的每一条文本消息,服务端都会随机地回应我们一句话。为了更好地展示Socket的工作机制,在服务端我们做了处理,使其能够和多个客户端同时建立连接并响应。先看一下服务端的设计,当Service启动时,会在线程中建立TCP服务,这里监听的是8688端口,然后就可以等待客户端的连接请求。当有客户端连接时,就会生成一个新的Socket,通过每次新创建的Socket就可以分别和不同的客户端通信了。服务端每收到一次客户端的消息就会随机回复一句话给客户端。当客户端断开连接时,服务端这边也会相应的关闭对应Socket并结束通话线程,这点是如何做到的呢?方法有很多,这里是通过判断服务端输入流的返回值来确定的,当客户端断开连接后,服务端这边的输入流会返回null,这个时候我们就知道客户端退出了。服务端的代码如下所示。

package com.nextvpu.myapplication;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

public class TCPServerService extends Service {

    private boolean mIsServiceDestoryed = false;
    private String[] mDefineMessages = new String[]{
            "你好啊,哈哈",
            "请问你叫什么名字呀?",
            "今天北京天气不错啊,shy",
            "你知道吗?我可是可以和多个人同事聊天的啊",
            "给你讲个笑话吧:据说爱笑的人运气不会太差,不知道真假。"
    };

    @Override
    public void onCreate() {
        new Thread(new TcpServer()).start();
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
       return null;
    }

    private class TcpServer implements Runnable {
        @Override
        public void run() {
            ServerSocket serverSocket = null;

            try {
                serverSocket = new ServerSocket(8688);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            while (!mIsServiceDestoryed){
                try {
                    final Socket client = serverSocket.accept();
                    System.out.println("accept");
                    new Thread(){
                        @Override
                        public void run() {
                            try {
                                responseClient(client);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void responseClient(Socket client) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        PrintWriter out = new PrintWriter(new BufferedWriter(
                new OutputStreamWriter(client.getOutputStream())),true);
        out.print("欢迎来到聊天室!");
        while (!mIsServiceDestoryed){
            String str = in.readLine();
            System.out.println("msg from client:"+str);
            if (str == null){
                break;
            }
            int i  = new Random().nextInt(mDefineMessages.length);
            String msg = mDefineMessages[i];
            out.println(msg);
            System.out.println("send:"+msg);
        }
        System.out.println("client quit");
        //关闭流
        MyUtils.close(out);
        MyUtils.close(in);
        client.close();
    }

    @Override
    public void onDestroy() {
        mIsServiceDestoryed = true;
        super.onDestroy();
    }
}

接着看一下客户端,客户端Activity启动时,会在onCreate中开启一个线程去连接服务端Socket,至于为什么用线程在前面已经做了介绍。为了确定能够连接成功,这里采用了超时重连的策略,每次连接失败后都会重新建立尝试建立连接。当然为了降低重试机制的开销,我们加入了休眠机制,即每次重试的时间间隔为1000毫秒。

 Socket socket = null;
        while (socket ==null){
            try {
                socket = new Socket("localhost",8688);
                mClientSocket = socket;
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
                mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
                System.out.println("connect server success");
            } catch (IOException e) {
                SystemClock.sleep(1000);
                System.out.println("connect tcp server failed, retry...");
//                e.printStackTrace();
            }

服务端连接成功以后,就可以和服务端进行通信了。下面的代码在线程中通过while循环不断地去读取服务端发送过来的消息,同时当Activity退出时,就退出循环并终止线程。

 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!MainActivity.this.isFinishing()){
                String msg = br.readLine();
                System.out.println("receive :" + msg);
                if (msg!=null){
                    String time = formatDateTime(System.currentTimeMillis());
                    final String showedMsg = "server"+time+" : "+msg+"\n";
                    mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG,showedMsg).sendToTarget();
                }

同时,当Activity退出时,还要关闭当前的Socket,如下所示。

 @Override
    protected void onDestroy() {
        if (mClientSocket!=null){
            try {
                mClientSocket.shutdownInput();
                mClientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

接着是发送消息的过程,这个就很简单了,这里不再详细说明。客户端的完整代码如下:

package com.nextvpu.myapplication;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private static final int MESSAGE_RECEIVE_NEW_MSG = 1;
    private static final int MESSAGE_SOCKET_CONNECTED = 2;
    private Button mSendButton;
    private TextView mMessageTextView;
    private EditText mMessageEditText;

    private PrintWriter mPrintWriter;
    private Socket mClientSocket;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MESSAGE_RECEIVE_NEW_MSG:{
                    mMessageTextView.setText(mMessageTextView.getText()+(String)msg.obj);
                    break;
                }
                case MESSAGE_SOCKET_CONNECTED:{
                    mSendButton.setEnabled(true);
                    break;
                }
                default:
                    break;
            }
        }
    };

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

        mMessageTextView = (TextView) findViewById(R.id.msg_container);
        mSendButton = (Button) findViewById(R.id.send);
        mSendButton.setOnClickListener(this);
        mMessageEditText = (EditText) findViewById(R.id.msg);
        Intent service = new Intent(this,TCPServerService.class);
        startService(service);
        new Thread(){
            @Override
            public void run() {
                connectTcpServer();
            }
        }.start();

    }

    @Override
    protected void onDestroy() {
        if (mClientSocket!=null){
            try {
                mClientSocket.shutdownInput();
                mClientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

    private void connectTcpServer() {
        Socket socket = null;
        while (socket ==null){
            try {
                socket = new Socket("localhost",8688);
                mClientSocket = socket;
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
                mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
                System.out.println("connect server success");
            } catch (IOException e) {
                SystemClock.sleep(1000);
                System.out.println("connect tcp server failed, retry...");
//                e.printStackTrace();
            }
        }

        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!MainActivity.this.isFinishing()){
                String msg = br.readLine();
                System.out.println("receive :" + msg);
                if (msg!=null){
                    String time = formatDateTime(System.currentTimeMillis());
                    final String showedMsg = "server"+time+" : "+msg+"\n";
                    mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG,showedMsg).sendToTarget();
                }

            }
            System.out.println("quit...");
            MyUtils.close(mPrintWriter);
            MyUtils.close(br);
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @SuppressLint("SimpleDateFormat")
    private String formatDateTime(long time) {
        return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
    }

    @Override
    public void onClick(View v) {
        if (v == mSendButton){
            final String msg = mMessageEditText.getText().toString();
            if (!TextUtils.isEmpty(msg)&&mPrintWriter!=null){
//                mPrintWriter.println(msg);
                mMessageEditText.setText("");
                String time = formatDateTime(System.currentTimeMillis());
                final String showedMsg = "self "+time+" : "+msg+"\n";
                mMessageTextView.setText(mMessageTextView.getText()+showedMsg);
            }
        }
    }
}

上述就是通过Socket来进行进程间通信的实例,除了采用TCP套接字,还可以采用UDP套接字。另外,上面的例子仅仅是一个示例,实际上通过Socket不仅仅能实现进程间的通信,还可以实现设备间的通信,当然前提是这些设备之间的IP地址互相可见,这其中又涉及许多复杂的概念,这里就不一一介绍了。

猜你喜欢

转载自blog.csdn.net/qq_20967339/article/details/82704696