美文网首页
进程间通信,数据流传递(AIDL、Socket)

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

作者: 沐左 | 来源:发表于2020-05-18 14:18 被阅读0次

    进程间通信

    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);
        }
    }
    

    相关文章

      网友评论

          本文标题:进程间通信,数据流传递(AIDL、Socket)

          本文链接:https://www.haomeiwen.com/subject/eupuohtx.html