进程间通信,数据流传递(AIDL、Socket)

进程间通信

Android 四大组件

Android 进程间通信可以通过Android 四大组件实现。

Activity

使用 Intent

Intent callIntent = new  Intent(Intent.ACTION_CALL, Uri.parse("tel:12345678" );  
startActivity(callIntent);

Content Provider

Content Provider可以跨进程访问其他应用程序中的数据(以Cursor对象形式返回),当然,也可以对其他应用程序的数据进行增、删、改操 作;

Content Provider返回的数据是二维表的形式

Broadcast

广播是一种被动跨进程通讯的方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。

Service

普通的Service并不能实现跨进程操作,我们可以使用


AIDL Service

Android 接口定义语言(AIDL),我们可以利用AIDL定义多个应用都认可的编程接口,方便二者使用进程间通信(IPC)。

在我们定义 AIDL 接口之前,我们需要明确一些事情

1、AIDL 接口的调用是直接的函数调用,如果涉及线程的切换,需要在接口调用方进行处理

2、AIDL 接口的实现必须基于完全的线程安全,调用方要对并发的情况做好处理

谷歌文档

AIDL的具体实现

正式进行开发

1、服务端APP创建.aidl文件

在 src 目录下右键创建 AIDL 文件

// IMyAIDLService.aidl
package com.zuo.aidlservice;


interface IMyAIDLService {
   //获取展示的数据
   String getShowStr();
}

创建完成后build一下,会生成以 .aidl 文件命名的 .java 接口文件。
在 项目 build/generated/aidl_source_output_dir/[debug/release]/compile*Aidl/out/包名/ 下。

生成的接口包含一个名为 Stub 的子类(例如,YourInterface.Stub),该子类是其父接口的抽象实现,并且会声明 .aidl 文件中的所有方法。

  • 重要提醒
    Stub 的子类中还会定义几个辅助方法,其中最值得注意的是 asInterface() ,该方法会接收 IBinder (**通常是传递给客户端 OnServiceConnected() 回调方法的参数 **),并返回 Stub 接口的实例。

补充说明

AIDL 支持的数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5fLk1tIi-1589773353400)(_v_images/20200512120919440_1925.png)]

当我们在接口方法中使用这些类型的时候,需要为各自的类型加入一条 import 语句,才能使用。


2、服务端APP实现aidl文件定义的接口

我们定义一个 Binder 类用来继承 aidl 接口文件的 Stub 子类,或者用匿名内部类的方式实现

    class MyBinder extends IMyAIDLService.Stub {

        @Override
        public String getShowStr() throws RemoteException {
            //todo 实现服务端的逻辑
            return "来自服务端的问好";
        }
    }

现在,binder 是 Stub 类的一个实例(一个 Binder),其定义了服务的远程过程调用 (RPC) 接口。
在下一步中,我们会向客户端公开此实例,以便客户端能与服务进行交互(Binder机制)。

简单理解Binder机制的原理


3、服务端APP向客户端APP公开接口

我们定义一个服务类,实现 onBind() 方法来公开我们的服务,onBind() 方法中返回 IBinder 接口的实现类(继承自 aidl 接口文件的 Stub 子类)

服务类路径为java代码路径 ,而非 aidl 文件路径

/**
 * 向客户端公开 IMyAIDLService 接口
 *
 * @author zuo
 * @date 2020/5/12 14:55
 */
public class MyAIDLService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IMyAIDLService.Stub {

        @Override
        public String getShowStr() throws RemoteException {
            //todo 实现服务端的逻辑
            return "来自服务端的问好";
        }
    }
}

现在当客户端APP中的组件(如 Activity)调用 bindService() 以连接此服务的时候,客户端APP的 onServiceConnected() 回调方法就会接收到服务端 onBind() 方法所返回的 binder 实例。

  • 注意事项

1、服务类需要在清单文件中注册

<service
    android:name=".MyAIDLService"
    android:enabled="true"
    android:exported="true" />

2、客户端必须拥有 IMyAIDLService 接口类的访问权限,才能调用上述服务
因此当客户端和服务不在同一个应用内时,客户端应用也必须包含.aidl 文件的副本。
(该文件会生成 android.os.Binder 接口,进而为客户端提供 AIDL 方法的访问权限)

3、**当客户端在 onServiceConnected() 回调中收到 IBinder 时,必须调用接口服务的asInterface方法,用来把返回的参数转换成 IMyAIDLService 类型,如

 iMyAIDLService= IMyAIDLService.Stub.asInterface(IBinder)

4、进程间传递对象

我们可以通过上述的 IPC 接口,在进程间传递实体对象,该实体对象需要支持 Parcelable 接口。

备注
如果需要创建 Parcelable 类的 .aidl 文件,请参考Rect.aidl 文件所示步骤

  • 如果我们需要传递 Bundle 参数
    当客户端传递过来一个 Bundle 数据时,我们在读取之前必须调用Bundle.setClassLoader(ClassLoader) 设置软件包的类加载器
    否则,即使您在应用中正确定义 Parcelable 类型,也会遇到 ClassNotFoundException。参考如下代码:
private final IRectInsideBundle.Stub binder = new IRectInsideBundle.Stub() {
    public void saveRect(Bundle bundle){
        bundle.setClassLoader(getClass().getClassLoader());
        Rect rect = bundle.getParcelable("rect");
        process(rect); // Do more with the parcelable.
    }
};

5、客户端调用IPC方法和服务端通信

客户端APP调用 aidl 接口实现和服务端APP的进程间通信。

  • 1、在项目的 src/目录中加入 .aidl 文件

我这里是直接将服务端APP的aidl 文件拷贝过来使用的

// IMyAIDLService.aidl
package com.zuo.aidlservice;


interface IMyAIDLService {
   //获取展示的数据
   String getShowStr();
}
  • 2、声明一个 IBinder 接口实例(基于 AIDL 生成)

同样是将服务端APP的文件拷贝过来使用,区别在于客户端只拷贝了 Binder 类,没有拷贝 Service 类。
** Binder 类必须要,没有则无法访问到服务端APP的 getShowStr() 方法。**

/**
 * IMyAIDLService 接口
 *
 * @author zuo
 * @date 2020/5/12 14:55
 */
public class MyBinder extends IMyAIDLService.Stub {

    @Override
    public String getShowStr() throws RemoteException {
        return "来自客户端的问好";
    }
}
  • 3、实现 ServiceConnection

在需要调用的地方,如 Activitty 实现ServiceConnection

    /**
     * 实现 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 4、调用 Context.bindService(),传入 ServiceConnection 实现

这里需要注意,Android5.0以后绑定启动Service考虑到安全原因,不允许隐式意图的方式启动,也就是说要给出一个明确的组件Service。
intent.setPackage(String packageName)或者intent.setComponent(ComponentName componentName)都可以显示设置组件处理意图。

    /**
     * 绑定服务,设置绑定后自动开启服务
     *
     * @return
     */
    private void bindService() {
        Intent intent = new Intent();
        intent.setAction("com.zuo.aidlservice.MyAIDLService");
        //待使用远程Service所属应用的包名
        intent.setPackage("com.zuo.aidlservice");
        try {
            bindService(intent, conn, BIND_AUTO_CREATE);
            isBound = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 5、在 onServiceConnected() 实现中,您将收到一个 IBinder 实例(名为 service)。调用 MyAIDLService.Stub.asInterface((IBinder)service),以将返回的参数转换为 MyAIDLService 类型。
    /**
     * 实现 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyAIDLService iMyAIDLService = IMyAIDLService.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 6、调用您在接口上定义的方法。

我们需要在调用方法的时候捕获 DeadObjectException 异常,该异常是系统在连接中断时抛出的。
我们还需要捕获 SecurityException 异常,这个异常是 IPC 方法调用中两个进程的 AIDL 定义发生冲突时,系统抛出的异常。

    /**
     * 实现 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyAIDLService iMyAIDLService = IMyAIDLService.Stub.asInterface(service);
            try {
                String showStr = iMyAIDLService.getShowStr();
                binding.text.setText(TextUtils.isEmpty(showStr) ? "返回错误!" : showStr);
            } catch (Exception e) {
                Log.i(TAG, "onServiceConnected: " + e.getMessage());
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 7、如要断开连接,请使用您的接口实例调用 Context.unbindService()
    @Override
    protected void onPause() {
        super.onPause();
        //解绑服务
        if (isBound) {
            try {
                unbindService(conn);
                isBound = false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

效果
先启动 AIDL_SERVICE APP ,然后启动 AIDL_Client APP,
点击AIDL_Client界面展示的 “Hello Worlf!”从AIDL_SERVICE获取展示内容

在这里插入图片描述

项目代码结构

12


LocalSocket & LocalServerSocket

LocalSocket

本地 socket 是在unix 域名空间创建一个套接字(非服务器)。

构造函数

  • LocalSocket() , 无参构造函数,创建一个 SOCKET_STREAM 类型的本地套接字
  • LocalSocket(int sockType),有参构造函数,创建对应类型的本地套接字

可以创建的类型
SOCKET_DGRAM -- 数据报,数据报是通过网络传输的数据的基本单元,包含一个报头(header)和数据本身,类似于 UDP
SOCKET_STREAM -- 流,类似于 TCP
SOCKET_SEQPACKET -- 顺序数据包


公共方法

1、bind(LocalSocketAddress bindpoint)

绑定套接字到本地地址上,该方法只能调用一次,如果已绑定的套接字实例继续调用该方法会报IOException("already bound")异常。

我们可以通过 isBound()方法来判断当前实例是否已经绑定。

2、close()

关闭当前的套接字

3、connect()

连接套接字到本地地址上,该方法有两个重载方法 connect(LocalSocketAddress endpoint)connect(LocalSocketAddress endpoint, int timeout)
区别在于一个可以设置连接超时时间。

同样的,如果已经绑定的套接字实例继续调用该方法会报IOException("already connected")异常

我们可以通过 isConnected()方法来判断当前实例是否已经绑定。

另外,如果套接字处于无效状态或者连接的地址不存在。也会报IOException异常

4、getAncillaryFileDescriptors() 、setFileDescriptorsForSend(FileDescriptor[] fds)

set 方法,发送一组文件描述,将在普通数据下一次写入时发送,并以单个辅助信息的方式到达。
get方法,获取一组文件描述,通过辅助信息返回的一组文件描述,FileDescriptor[] 。

文件描述只能和常规数据一起传递,因此此方法只能在读取操作后返回非null。

5、getFileDescriptor()

返回文件描述符;如果尚未打开/已经关闭,则返回null

6、getInputStream()

返回套接字实例的输入流,InputStream

7、getOutputStream()

返回套接字实例的输出流,OutputStream

8、getLocalSocketAddress()

返回套接字绑定的地址,可能为 null 。LocalSocketAddress

9、getPeerCredentials()

返回套接字的证书,包含 pid 、uid 、gid 。已 root 的设备可能被篡改。

10、其他方法

  • getReceiveBufferSize() ,接收缓存的size
  • setReceiveBufferSize(int size) ,设置缓存的size
  • getSendBufferSize() ,发送缓存的size
  • setSendBufferSize(int n) ,设置发送缓存的size
  • getRemoteSocketAddress() ,获取远端socket 的地址
  • getSoTimeout() , 获取读取超时的时间
  • setSoTimeout(int n) , 设置读取超时的时间
  • isBound() ,socket 是否已经绑定
  • isClosed() ,socket 是否已经关闭
  • isConnected() ,socket 是否已经连接
  • isInputShutdown() ,是否已经终止输入
  • shutdownInput(),终止socket的输入
  • isOutputShutdown() , 是否已经终止输出
  • shutdownOutput(),终止socket的输出

相关概念

1、LocalSocketAddress

两个构造函数,LocalSocketAddress(String name)LocalSocketAddress(String name, Namespace namespace)

区别在于是否指定命名空间,不指定时默认为:ABSTRACT

可选择的命名空间类型
ABSTRACT -- Linux 中抽象的命名空间
RESERVED -- Android保留命名空间,位于/ dev / socket中。 只有init进程可以在此处创建套接字。
FILESYSTEM -- 以普通文件系统路径命名的套接字。

2、pid 、uid 、gid

Android中UID、GID和PID的讲解

Linux中的概念

  • UID
    在Linux中用户的概念分为:普通用户、根用户和系统用户。
    普通用户:表示平时使用的用户概念,在使用Linux时,需要通过用户名和密码登录,获取该用户相应的权限,其权限具体表现在对系统中文件的增删改查和命令执行的限制,不同用户具有不同的权限设置,其UID通常大于500。
    根用户:该用户就是ROOT用户,其UID为0,可以对系统中任何文件进行增删改查处理,执行任何命令,因此ROOT用户极其危险,如操作不当,会导致系统彻底崩掉。
    系统用户:该用户是系统虚拟出的用户概念,不对使用者开发的用户,其UID范围为1-499,例如运行MySQL数据库服务时,需要使用系统用户mysql来运行mysqld进程。

  • GID
    GID顾名思义就是对于UID的封装处理,就是包含多个UID的意思,实际上在Linux下每个UID都对应着一个GID。设计GID是为了便于对系统的统一管理,例如增加某个文件的用户权限时,只对admin组的用户开放,那么在分配权限时,只需对该组分配,其组下的所有用户均获取权限。同样在删除时,也便于统一操作。

除了UID和GID外,还包括其扩展的有效的用户、组(euid、egid)、文件系统的用户、组(fsuid、fsgid)和保存的设置用户、组(suid、sgid)等。

  • PID
    系统在程序运行时,会为每个可执行程序分配一个唯一的进程ID(PID),PID的直接作用是为了表明该程序所拥有的文件操作权限,不同的可执行程序运行时互不影响,相互之间的数据访问具有权限限制。

Android 中的概念

在Android中一个UID的对应的就是一个可执行的程序,对于普通的程序其UID就是对应与GID,程序在Android系统留存期间,其UID不变。
PID 同样是进程的 ID。

3、FileDescriptor

文件描述符,用来表示打开的文件、打开的套接字或者其他流。
主要用途是创建一个输入流或者输出流,FileInputStream or FileOutputStream。


LocalServerSocket

在Linux抽象命名空间中创建一个 在 UNIX域名 边界内的套接字

构造函数

  • LocalServerSocket(String name),创建一个监听指定地址的新服务器套接字,该地址 是 Linux 抽象命名空间中的,不是手机的文件管理系统
    public LocalServerSocket(String name) throws IOException
    {
        impl = new LocalSocketImpl();

        impl.create(LocalSocket.SOCKET_STREAM);

        localAddress = new LocalSocketAddress(name);
        impl.bind(localAddress);

        impl.listen(LISTEN_BACKLOG);
    }
  • LocalServerSocket(FileDescriptor fd),从一个已经创建并绑定了的文件描述符中创建服务器套接字,创建后 listen 将被立即调用
    public LocalServerSocket(FileDescriptor fd) throws IOException
    {
        impl = new LocalSocketImpl(fd);
        impl.listen(LISTEN_BACKLOG);
        localAddress = impl.getSockAddress();
    }

公共方法

1、accept()

接收一个新的socket连接,阻塞直到这个新的连接到达。

返回一个 新连接的套接字,LocalSocket。

2、close()

关闭服务器套接字

3、getFileDescriptor()

返回文件描述符;如果尚未打开/已经关闭,则返回null

4、getLocalSocketAddress()

获取套接字的本地地址


LocalSocket 使用示例

服务端APP

LocalServerSocket实现类

  • 和客户端进行数据的收发
  • 实现Runnable接口,在工作线程中持续进行消息接收的监听,并将接收到的消息通过handler发送给外部
  • 实现发送方法
  • 实现close方法
/**
 * 和客户端进行数据收发
 * <p>
 * 传递的数据为 二进制数组 byte[]
 *
 * @author zuo
 * @date 2020/5/14 15:08
 */
public class SocketServerImpl implements Runnable {
    private static final String TAG = "SocketServerImpl";
    private String localSocketAddress = "com.zuo.service";
    private BufferedOutputStream os;
    private BufferedInputStream is;
    public static final int bufferSizeOutput = 1024 * 1024;
    LocalServerSocket server;
    LocalSocket client;
    Handler handler;

    public SocketServerImpl(Handler handler) {
        this.handler = handler;
    }

    @Override
    public void run() {
        Log.i(TAG, "Server isOpen");
        try {
            if (null == server) {
                server = new LocalServerSocket(localSocketAddress);
            }
            if (null == client) {
                client = server.accept();
                Log.i(TAG, "Client Connected");
            }
            Credentials cre = client.getPeerCredentials();
            Log.i(TAG, "ClientID:" + cre.getUid());
            os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
            is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        while (null != is) {
            try {
                if (is.available() <= 0) continue;
                Message msg = handler.obtainMessage();
                msg.obj = is;
                msg.arg1 = 1;
                handler.sendMessage(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送数据
     *
     * @param data
     */
    public void send(byte[] data) throws Exception {
        if (null != os) {
            os.write(data);
            os.flush();
        }
    }

    /**
     * 关闭监听
     */
    public void close() {
        try {
            if (null != os) {
                os.close();
                os = null;
            }
            if (null != is) {
                is.close();
                is = null;
            }
            if (null != client) {
                client.close();
                client = null;
            }
            if (null != server) {
                server.close();
                server = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

服务端活动界面

  • 启动 SocketServer
  • 处理接收到的客户端信息,
  • 展示活动界面,并将数据发送给客户端
/**
 * @author zuo
 * @date 2020/5/18 11:01
 */
public class MainActivity extends AppCompatActivity {
    
    private SocketServerImpl socketServer;
    private ActivityMainBinding binding;
    private List<Integer> data;
    @IntRange(from = 0, to = 3)
    private int index = 0;

    //持续接收客户端反馈信息
    private StringBuilder buffer = new StringBuilder();
    Handler handler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.arg1 == 1) {
                SocketParseBean bean = null;
                try {
                    bean = SendDataUtils.parseSendData((BufferedInputStream) msg.obj);
                    if (null == bean || TextUtils.isEmpty(bean.getInfo())) return false;
                    showImg();
                } catch (Exception e) {
                    return false;
                }
                buffer.append(bean.getInfo());
                buffer.append("\r\n");
                showSocketMsg();
            }
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setPresenter(new Presenter());
        initData();
        startSocketServer();
    }

    private void showSocketMsg() {
        if (null != binding) {
            binding.backMsgShow.setText("客户端消息:" + buffer.toString());
        }
    }

    private void startSocketServer() {
        socketServer = new SocketServerImpl(handler);
        new Thread(socketServer).start();
    }

    private void initData() {
        data = new ArrayList<>();
        data.add(R.drawable.kb890);
        data.add(R.drawable.kb618);
        data.add(R.drawable.kb224);
    }

    private void showImg() {
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), data.get(index));
        binding.imgShow.setImageBitmap(bmp);
        binding.indexShow.setText((index + 1) + "/" + data.size());
        String hint = "服务端正在展示第 " + (index + 1) + " 张照片";
        sendData(hint, bmp);
    }

    public void sendData(final String hint, final Bitmap bmp) {
        if (null != socketServer) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] array = null;
            try {
                if (null != bmp) {
                    bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
                    array = baos.toByteArray();
                }
                byte[] bytes = SendDataUtils.makeSendData(hint, array);
                socketServer.send(bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != socketServer) {
            socketServer.close();
        }
    }

    public class Presenter {

        public void last(View view) {
            if (index <= 0) {
                Toast.makeText(MainActivity.this, "没有上一张了!", Toast.LENGTH_SHORT).show();
                return;
            }
            index--;
            showImg();
        }

        public void next(View view) {
            if (index >= 2) {
                Toast.makeText(MainActivity.this, "没有下一张了!", Toast.LENGTH_SHORT).show();
                return;
            }
            index++;
            showImg();
        }
    }

}

客户端APP

LocalSocket实现类

  • 和服务端进行数据的收发
  • 实现Runnable接口,在工作线程中持续进行消息接收的监听,并将接收到的消息通过handler发送给外部
  • 实现发送方法
  • 实现close方法
/**
 * 和服务端进行数据收发
 *
 * @author zuo
 * @date 2020/5/14 15:08
 */
public class SocketClientImpl implements Runnable {
    private static final String TAG = "SocketClientImpl";
    private String localSocketAddress = "com.zuo.service";
    private BufferedOutputStream os;
    private BufferedInputStream is;
    private int timeout = 30000;
    public static final int bufferSizeOutput = 1024 * 1024;
    private LocalSocket client;
    private Handler handler;

    public SocketClientImpl(Handler handler) {
        this.handler = handler;
    }

    @Override
    public void run() {
        Log.i(TAG, "Client isOpen");
        try {
            if (null == client) {
                client = new LocalSocket();
                client.connect(new LocalSocketAddress(localSocketAddress));
                client.setSoTimeout(timeout);
                Log.i(TAG, "Server Connected");
            }
            os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
            is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        //将接收到的数据发送出去
        while (null != is) {
            try {
                if (is.available() <= 0) continue;
                Message msg = handler.obtainMessage();
                msg.obj = is;
                msg.arg1 = 1;
                handler.sendMessage(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送数据
     *
     * @param data
     */
    public void send(byte[] data) throws Exception {
        if (null != os) {
            os.write(data);
            os.flush();
        }
    }

    /**
     * 关闭监听
     */
    public void close() {
        try {
            if (null != os) {
                os.close();
                os = null;
            }
            if (null != is) {
                is.close();
                is = null;
            }
            if (null != client) {
                client.close();
                client = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

客户端活动界面

  • 启动 SocketClient
  • 处理接收到的服务端信息,
  • 展示活动界面,并将数据发送给服务端
/**
 * @author zuo
 * @date 2020/5/18 11:29
 */
public class MainActivity extends AppCompatActivity {

    private SocketClientImpl socketClient;
    private ActivityMainBinding binding;

    //持续接收服务端反馈信息
    private StringBuilder buffer = new StringBuilder();
    Handler handler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.arg1 == 1) {
                SocketParseBean bean = null;
                try {
                    bean = SendDataUtils.parseSendData((BufferedInputStream) msg.obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (null == bean || TextUtils.isEmpty(bean.getInfo())) return false;
                buffer.append(bean.getInfo());
                buffer.append("\r\n");
                showSocketMsg(bean.getData());
            }
            return true;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setPresenter(new Presenter());
        startSocketClient();
    }

    private void showSocketMsg(final byte[] data) {
        if (null != binding) {
            binding.backMsgShow.setText(buffer.toString());
        }
        showImg(data);
    }

    private void startSocketClient() {
        socketClient = new SocketClientImpl(handler);
        new Thread(socketClient).start();
    }

    private void showImg(byte[] data) {
        if (null == data) return;
        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
        binding.imgShow.setImageBitmap(bitmap);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != socketClient) {
            socketClient.close();
        }
    }

    public void sendData2Server(final String hint, final Bitmap bmp) throws Exception {
        if (null != socketClient) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] array = null;
            if (null != bmp) {
                bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
                array = baos.toByteArray();
            }
            byte[] bytes = SendDataUtils.makeSendData(hint, array);
            socketClient.send(bytes);
        }
    }

    public class Presenter {

        public void sendData(View view) {
            String text = binding.clientInput.getText().toString().trim();
            if (TextUtils.isEmpty(text)) {
                Toast.makeText(MainActivity.this, "消息内容不能为空!", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                sendData2Server(text, null);
            } catch (Exception e) {
                Toast.makeText(MainActivity.this, "消息发送失败!", Toast.LENGTH_SHORT).show();
            }
        }

    }
}

共用工具类

封装、解析流数据

  • 字符串信息统一使用 utf-8 编码格式,防止出现乱码
  • 提供信息流数据封装方法及数据流解析方法
/**
 * LocalSocket 传输数据(封装、解析)工具类
 * <p>
 * 数据传输规则:
 * [0,7)  -- infoSize
 * [7,14) -- dataSize
 * [14,14+infoSize) -- info
 * [14+infoSize,14+infoSize+dataSize)  -- data
 *
 * @author zuo
 * @date 2020/5/14 19:20
 */
public class SendDataUtils {
    /**
     * 对应数据的 size ,7 位 (9.5M)
     */
    private static final int infoSize = 7;
    private static final int dataSize = 7;

    /**
     * 封装 LocalSocket 发送的数据
     *
     * @param info -- 需要发送的字符串数据
     * @param data -- 需要发送的字节流数据
     * @return 封装后的字节流数据
     */
    public static byte[] makeSendData(@NonNull String info, byte[] data) throws Exception {
        //文本信息
        Charset charset_utf8 = Charset.forName("utf-8");
        ByteBuffer buff = charset_utf8.encode(info);
        byte[] infoBytes = buff.array();
        int infoLength = infoBytes.length;
        byte[] headSizeBytes = String.valueOf(infoLength).getBytes();
        int dataLength = data == null ? 0 : data.length;
        byte[] dataSizeBytes = String.valueOf(dataLength).getBytes();
        int totalSize = infoSize + dataSize + infoLength + dataLength;
        byte[] output = new byte[totalSize];
        //1、头部信息(info size)
        System.arraycopy(headSizeBytes, 0, output, 0, headSizeBytes.length);
        //2、头部信息(data size)
        System.arraycopy(dataSizeBytes, 0, output, infoSize, dataSizeBytes.length);
        //2、info 信息
        System.arraycopy(infoBytes, 0, output, infoSize + dataSize, infoLength);
        if (dataLength > 0) {
            //拷贝 data 信息
            System.arraycopy(data, 0, output, infoSize + dataSize + infoLength, dataLength);
        }
        return output;
    }

    /**
     * 解析 LocalSocket 接收到的数据
     *
     * @param is -- 待解析的输入流
     * @return 解析后的数据
     * @throws Exception
     */
    public static SocketParseBean parseSendData(BufferedInputStream is) throws Exception {
        if (null == is || is.available() <= 0) return null;
        //拿到info信息的size
        byte[] infoSizeByte = new byte[infoSize];
        is.read(infoSizeByte);
        String infoLength = new String(infoSizeByte);
        String infoSizeStr = infoLength.trim();
        Integer infoSize = Integer.valueOf(infoSizeStr);
        //拿到data的size
        byte[] dataSizeByte = new byte[dataSize];
        is.read(dataSizeByte);
        String dataLength = new String(dataSizeByte);
        String dataSizeStr = dataLength.trim();
        Integer dataSize = Integer.valueOf(dataSizeStr);
        //数据读取
        SocketParseBean parseBean = new SocketParseBean();
        if (infoSize <= 0 && dataSize <= 0) {
            return parseBean;
        }
        //读取info
        byte[] infoByte = new byte[infoSize];
        is.read(infoByte, 0, infoSize);
        String s = new String(infoByte, "utf-8");
        parseBean.setInfo(s.trim());
        //读取data
        if (dataSize > 0) {
            byte[] buffer = new byte[dataSize];
            is.read(buffer, 0, dataSize);
            parseBean.setData(buffer);
        }
        return parseBean;
    }

}

解析数据封装实体

/**
 * 解析socket服务传递的数据
 *
 * @author zuo
 * @date 2020/5/15 17:03
 */
public class SocketParseBean {

    private String info;
    private byte[] data;

    public SocketParseBean() {
    }

    public SocketParseBean(String info, byte[] data) {
        this.info = info;
        this.data = data;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
}

项目结构

在这里插入图片描述


LocalSocket交互效果展示

  • 客户端向服务端发送文本消息时,服务端将正在展示的照片及相关信息发送给客户端
  • 服务端切换照片时,将正在展示的照片及相关信息发送给客户端
在这里插入图片描述

使用Socket

LocalSocket 在某些设备上出现 权限拒绝等错误,将上述demo中的 LocalSocket 替换为 Socket

SocketClientImpl

替换后的代码

            if (null == client) {
//                client = new LocalSocket();
                client = new Socket("localhost", 8080);
//                client.connect(new LocalSocketAddress(localSocketAddress));
                client.setSoTimeout(timeout);
                Log.i(TAG, "Server Connected");
            }

SocketServerImpl

替换后的代码

            if (null == server) {
//                server = new LocalServerSocket(localSocketAddress);
                server = new ServerSocket(8080);
            }

流里面取每一帧的策略

//25 Kb 的缓冲区
int bufferSizeOutput = 1024 * 25;
os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
//yuv data的长度 = 视频帧width*height*1.5
int srcWidth = 480, srcHeight = 320;
int totalSize = srcWidth * srcHeight * 3 / 2;
int tmpSize = 0;
byte[] buffer = new byte[bufferSizeOutput];
while (client.isConnected()) {
    if (is.read() == 0xA0) {
        ByteArrayOutputStream tempStream = new ByteArrayOutputStream();
        while (tmpSize < totalSize) {
            int len = is.read(buffer);
            tmpSize += len;
            tempStream.write(buffer, 0, len);
        }
        Frame frame = new Frame(tempStream.toByteArray(), srcWidth, srcHeight);
        LiveStreamRepository.getInstance().addFrame(frame);
        tmpSize = 0;
        tempStream.close();
        Log.e(TAG, "receive " + frame.toString());
    }
}

使用 DatagramSocket

使用数据报套接字实现进程间通信,客户端和服务端应用各自监听自己的端口。实现类SocketTextImpl

客户端和服务端使用同一个类,区别在于监听和发送数据包的端口不同

/**
 * 采用数据包的方式发送文本类型的数据
 * 本实例区别于 SocketClientImpl ,仅用作于文本信息的传递,采用 UDP 协议
 *
 * @author zuo
 * @date 2020/5/14 15:08
 */
public class SocketTextImpl implements Runnable {
    private static final String TAG = "SocketClientTextImpl";
    public static final int bufferSize = 1024 * 1024;
    private DatagramSocket socket;
    private final int SERVER_PORT = 8090;
    private final int CLIENT_PORT = 8091;

    public SocketTextImpl() {
    }

    @Override
    public void run() {
        Log.i(TAG, "Client isOpen");
        try {
            if (null == socket) {
                //监听对应端口
                socket = new DatagramSocket(SERVER_PORT, InetAddress.getLocalHost());
            }
        } catch (IOException e1) {
            Log.i(TAG, e1.getMessage());
            e1.printStackTrace();
        }
        //接收信息
        while (true) {
            byte[] buffer = new byte[bufferSize];
            DatagramPacket recDp = new DatagramPacket(buffer, buffer.length);
            try {
                //定义1M的文本消息缓存,如果消息大于1M,会被截断
                socket.receive(recDp);
                String recMsg = new String(buffer, 0, recDp.getLength());
                LiveStreamRepository.getInstance().addData(recMsg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送给客户端的数据,使用客户端监听的端口
     *
     * @param data
     */
    public void send(String data) throws Exception {
        if (null != socket) {
            byte[] bytes = data.getBytes();
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), CLIENT_PORT);
            socket.send(packet);
        }
    }

    /**
     * 关闭监听
     */
    public void close() {
        try {
            if (null != socket) {
                socket.close();
                socket = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

其他

可用端口范围

一个有效的端口整数值:0 --65535

  • 0~1023:分配给系统的端口号
  • 1024~49151:登记端口号,主要是让第三方应用使用
  • 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。

在Socket使用时,可以用1024~65535的端口号


辅助类,数据存储队列

/**
 * 无人机互联,数据存储队列
 *
 * @author zuo
 * @date 2020/5/19 14:13
 */
public class LiveStreamRepository {
    //队列,可存储20帧数据
    private int mQueueSize = 10;
    private int mBufferSize = 5;

    private ArrayBlockingQueue<String> mQueue = new ArrayBlockingQueue<>(mQueueSize);


    private LiveStreamRepository() {
    }

    private final static class UavVideoInfoInstanceHolder {
        private static final LiveStreamRepository ins = new LiveStreamRepository();
    }

    public static LiveStreamRepository getInstance() {
        return UavVideoInfoInstanceHolder.ins;
    }

    public String getData() {
        return mQueue.poll();
    }

    public boolean addData(String data) {
        //如果插入失败(),移除前5帧
        if (mQueue.size() == mQueueSize) {
            for (int i = 0; i < mBufferSize; i++) {
                mQueue.remove(i);
            }
        }
        return mQueue.offer(data);
    }
}