读完本文你将了解:

上篇文章 Android 进阶7:进程通信之 AIDL 中我们虽然跨进程通信成功,但是还是有很多疑问的,比如:

  • AIDL 帮我们做了什么?
  • 为什么要这么写?
  • 什么是 Binder?

知其然还要知其所以然,一切都要从 Binder 讲起。

IBinder

Binder 继承自 IBinder,所以我们先来了解下它。

public class Binder implements IBinder {...}
public interface IBinder {...}

IBinder 是一个接口,它代表了一种跨进程传输的能力。只要实现了这个接口,就能将这个对象进行跨进程传递。

IBinder 是高性能、轻量级远程调用机制的核心部分,它定义了远程操作对象的基本接口。

这些方法中最关键的一个是 transact():

public boolean transact(int code, Parcel data, Parcel reply, int flags)
    throws RemoteException;

与它对应的是 Binder.onTransact()

protected boolean onTransact(int code, Parcel data, Parcel reply,
        int flags) throws RemoteException {...}

可以看到这两个方法非常相似,介绍一下方法中的各个参数:

  • code:要执行的动作,类似 Handler 的 msg.what,IBinder 中定义了以下几个 code

    • PING_TRANSACTION,表示要调用 pingBinder() 方法
    • DUMP_TRANSACTION,表示要获取 Binder 内部状态
    • SHELL_COMMAND_TRANSACTION,执行一个 shell 命令
    • INTERFACE_TRANSACTION,询问被调用方的接口描述符号
    • TWEET_TRANSACTION
    • LIKE_TRANSACTION
    • 如果我们需要自定义 code,code 的值范围需要在 FIRST_CALL_TRANSACTION(0x00000001) 和 LAST_CALL_TRANSACTION(0x00ffffff) 之间
  • data, reply:传入的参数和返回的值
  • flags:表示是否需要阻塞等待返回值,有两个值
    • 0
    • FLAG_ONEWAY (0x00000001),表示 Client 的 transact() 是单向调用,执行后立即返回

①经常的场景是,我们调用 IBinder.transact() 给一个 IBinder 对象发送请求,然后经过 Binder Binder.onTransact() 得到调用,接着远程操作的目标得到对应的调用。

这个过程不仅在同一进程中可以进行,在跨进程(IPC)间也可以完成。

IBinder.transact() 方法是同步的,它被调用后一直到 Binder.onTransact() 调用完成后才返回。

②通过 IBinder.transact() 方法传输的数据被保存为一个 Parcel 对象,Parcel 中保存了数据以及描述数据的元数据,元数据在缓存区中保持了 IBinder 对象的引用,这样不同进程都可以访问同一个数据。

因此在一个 IBinder 对象写入 Parcel 对象然后发送到另一个进程时,另外那进程将这个 IBinder 对象发送回去时,原本进程接收到的 IBinder 对象和开始发送出去的是同一个引用。

在跨进程传输后引用没有改变,这是非常关键的一点,这就使得 IBinder/Binder 对象在跨进程通信时可以作为唯一的标识(比如作为 token 什么的)。

③系统在每个进程中都有一个处理事物的线程池,这些线程用于调度其他进程对当前进程的跨进程访问。

比如说进程 A 对进程 B 发起 IPC 时,A 中调用 transact() 的线程会阻塞。B 中的事物线程池收到 A 的 IPC,调用目标对象的 Binder.onTransact() 方法,然后返回带结果的 Parcel。一旦接收到结果,A 中阻塞的线程得以继续执行。

这个过程和线程通信非常相似吧。

④Binder 机制还支持进程间的递归调用

比如,进程 A 向进程 B 发起 IPC,而进程 B 在其 Binder.onTransact() 中又用 transact() 向进程 A 发起 IPC,那么进程 A 在等待它发出的调用返回的同时,也会响应 B 的调用,对调用的对象执行 Binder.onTransact() 方法。

这种机制可以让我们觉得到跨进程的调用与进程内的调用没什么区别,这是非常重要的。

⑤在跨进程通信时,我们常常想要知道另外进程是否可用,IBinder 提供了三个检查的方法:

  1. transact()

    • 当你调用的 IBinder 所在进程不存在时,会抛出 RemoteException 异常
  2. pingBinder()
    • 当远程进程不存在时该方法返回 false
  3. linkToDeath()
    • 这个方法可以向 IBinder 中注册一个 IBinder.DeathRecipient,它将在 IBinder 所在的进程退出时被调用
/**
 * 检查远程 Binder 对象是否存在
 *
 * 当不存在时返回 false
 */
public boolean pingBinder();
/**
 * 注册一个 Binder 销毁的监听
   如果一个 Binder 被销毁(通常是它所在的进程被关闭),会回调 DeathRecipient 的 BinderDied 方法
 * 注意,只会监听远程的 Binder,本地 Binder 一般不会销毁,除非当前进程退出
 *
 * 如果要注册的 Binder 进程已经销毁,就抛出 RemoteException
 */
public void linkToDeath(DeathRecipient recipient, int flags)
        throws RemoteException;

/**
 * linkToDeath 注册监听回调的接口
 */
public interface DeathRecipient {
    public void binderDied();
}

Binder

官方文档中建议:

日常开发中一般不需要我们再实现 IBinder,直接使用系统提供的 Binder 即可

Binder 实现了 IBinder 定义的操作,它是 Android IPC 的基础,平常接触到的各种 Manager(ActivityManager, ServiceManager 等),以及绑定 Service 时都在使用它进行跨进程操作。

它的存在不会影响一个应用的生命周期,只要创建它的进程在运行它就一直可用。

通常我们需要在顶级的组件(Service, Activity, ContentProvider)中使用它,这样系统才知道你的进程应该一直被保留。

下面介绍 Binder 的几个关键方法:

实现 IBindertransact() 方法:

public final boolean transact(int code, Parcel data, Parcel reply,
        int flags) throws RemoteException {
    if (false) Log.v("Binder", "Transact: " + code + " to " + this);

    if (data != null) {
        data.setDataPosition(0);
    }
    boolean r = onTransact(code, data, reply, flags);
    if (reply != null) {
        reply.setDataPosition(0);
    }
    return r;
}

可以看到,这个方法就是调用 onTransact() ,然后将返回的结果再返回回去。

接着看看 onTransact() 方法:

protected boolean onTransact(int code, Parcel data, Parcel reply,
        int flags) throws RemoteException {
    if (code == INTERFACE_TRANSACTION) {    //获取接口描述
        reply.writeString(getInterfaceDescriptor());
        return true;
    } else if (code == DUMP_TRANSACTION) {    //获取当前状态
        ParcelFileDescriptor fd = data.readFileDescriptor();
        String[] args = data.readStringArray();
        if (fd != null) {
            try {
                dump(fd.getFileDescriptor(), args);
            } finally {
                IoUtils.closeQuietly(fd);
            }
        }
        // Write the StrictMode header.
        if (reply != null) {
            reply.writeNoException();
        } else {
            StrictMode.clearGatheredViolations();
        }
        return true;
    } else if (code == SHELL_COMMAND_TRANSACTION) {    //执行 shell 脚本
        ParcelFileDescriptor in = data.readFileDescriptor();
        ParcelFileDescriptor out = data.readFileDescriptor();
        ParcelFileDescriptor err = data.readFileDescriptor();
        String[] args = data.readStringArray();
        ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data);
        try {
            if (out != null) {
                shellCommand(in != null ? in.getFileDescriptor() : null,
                        out.getFileDescriptor(),
                        err != null ? err.getFileDescriptor() : out.getFileDescriptor(),
                        args, resultReceiver);
            }
        } finally {
            IoUtils.closeQuietly(in);
            IoUtils.closeQuietly(out);
            IoUtils.closeQuietly(err);
            // Write the StrictMode header.
            if (reply != null) {
                reply.writeNoException();
            } else {
                StrictMode.clearGatheredViolations();
            }
        }
        return true;
    }
    return false;
}

也没看出什么特别的,系统的 Binder.onTransact() 方法只定义了系统要进行的操作,我们如果创建自己的 Binder 时,就需要重写这个方法,根据 code 对传入的参数 data 做相应的处理,然后写入 reply,这样就能返回操作后的数据。

另外一个关键的方法 attachInterface

public void attachInterface(IInterface owner, String descriptor) {
    mOwner = owner;
    mDescriptor = descriptor;
}
/* mObject is used by native code, do not remove or rename */
private long mObject;
private IInterface mOwner;
private String mDescriptor;

这个方法的作用是将一个描述符、特定的 IInterface 与当前 Binder 绑定起来,这样后续调用 queryLocalInterface 就可以拿到这个 IInterface,那 IInterface 又是什么呢?

public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

其实看名字就可以大概猜出来,IInterface 应该就是进程间通信定义的通用接口,我们通过定义接口,然后再服务端实现接口、客户端调用接口,就可以实现跨进程通信。

IInterface 里只定义了一个 asBinder() 方法,这个方法可以返回当前接口关联的 Binder 对象。

Binder 通信机制

上面介绍了 Binder 类以及相关的方法,但是这只是 Binder 机制的最基础部分。

我们平常看的文章或者面试时,说的 Binder 其实是范围更大的整个 “Binder 消息通信机制”。

借用老罗的 Android进程间通信(IPC)机制Binder简要介绍和学习计划 中对 Binder 通信机制的介绍:

在 Android 系统的 Binder 机制中,由四个组件组成,分别是:

  • Client
  • Server
  • ServiceManager:提供辅助管理 Server 的功能
  • Binder 驱动程序:整个机制的核心

Binder 驱动

驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作。

我们知道,在 Linux 系统中,内存空间分为两部分:

  • 用户空间:运行着应用程序
  • 内核空间:运行着系统内核和驱动

用户空间中的进程无法直接访问内核空间,需要通过上图中的 System Call Interface (系统调用接口),通过这个统一入口,所有资源访问都是在内核的控制下执行,这样可以避免用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。

同样的,用户空间中的进程直接也不可以直接访问数据,需要通过内核空间进行中转。

在 Binder 机制中,由 Binder 驱动负责完成这个中转操作,主要过程如下:

  • 当 Client 向 Server 发起 IPC 请求时,Client 会先将请求数据从用户空间拷贝到内核空间
  • 数据被拷贝到内核空间之后,驱动程序将内核空间中的数据拷贝到 Server 位于用户空间的缓存中

这样,就成功的将 Client 进程中的请求数据传递到了 Server 进程中。

实际上,Binder 驱动是整个 Binder 机制的核心。除了实现数据传输之外,Binder 驱动还是实现线程控制(通过中断等待队列实现线程的等待/唤醒),以及 UID/PID 等安全机制的保证。

http://wangkuiwu.github.io/2014/09/01/Binder-Introduce/

Service Manager

ServiceManager 运行在用户空间,它负责管理 Service 注册与查询。

看下 ServiceManager 的代码:

public final class ServiceManager {
    private static final String TAG = "ServiceManager";

    private static IServiceManager sServiceManager;
    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();

    private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }

        // Find the service manager
        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
        return sServiceManager;
    }

    /**
     * 根据 Service 名称获取 Service
     */
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                //如果不存在就去 IServiceManager 中找,这时可能会阻塞
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

    /**
     * 添加一个 Service 到 Manager 中
     */
    public static void addService(String name, IBinder service) {
        try {
            getIServiceManager().addService(name, service, false);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }

    /**
     * 添加一个 Service 到 Manager 中,如果 allowIsolated 为 true 表示运行在沙盒中的进程也可以访问这个 Service

     */
    public static void addService(String name, IBinder service, boolean allowIsolated) {
        try {
            getIServiceManager().addService(name, service, allowIsolated);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }

    /**
     * Retrieve an existing service called @a name from the
     * service manager.  Non-blocking.
     */
    public static IBinder checkService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().checkService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in checkService", e);
            return null;
        }
    }

    /**
     * 获取 Service 列表
     */
    public static String[] listServices() {
        try {
            return getIServiceManager().listServices();
        } catch (RemoteException e) {
            Log.e(TAG, "error in listServices", e);
            return null;
        }
    }

    /**
     * 当前进程首次被 activity manager 创建时调用这个方法
     */
    public static void initServiceCache(Map<String, IBinder> cache) {
        if (sCache.size() != 0) {
            throw new IllegalStateException("setServiceCache may only be called once");
        }
        sCache.putAll(cache);
    }
}

可以看到 ServiceManager 提供了 Service 的添加和查询,其中主要操作都是通过 IServiceManager,它是何方神圣?

public interface IServiceManager extends IInterface
{
    /**
     * 获取一个 Service,不存在就会阻塞几秒
     */
    public IBinder getService(String name) throws RemoteException;

    /**
     * 不阻塞的获取 Service
     */
    public IBinder checkService(String name) throws RemoteException;

    public void addService(String name, IBinder service, boolean allowIsolated)
                throws RemoteException;

    public String[] listServices() throws RemoteException;

    /**
     * 为 Service Manager 添加权限,具体作用暂不追究
     */
    public void setPermissionController(IPermissionController controller)
            throws RemoteException;

    static final String descriptor = "android.os.IServiceManager";

    //定义了一些用于调用 transact() 方法的 code
    int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
    int CHECK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1;
    int ADD_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
    int LIST_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
    int CHECK_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4;
    int SET_PERMISSION_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5;
}

可以看到 IServiceManager 是一个接口,它定义了管理 Service 的一些方法,同时继承了 IInterface。最常见的实现是 BnServiceManager.getDefault()

Binder 机制跨进程通信流程

上面两节简单介绍了 Binder 机制中非常重要的两部分,ServiceManager 和 Binder 驱动。

在 Binder 机制的四个部分中, Client、Server 和 ServiceManager 运行在用户空间,Binder 驱动程序运行内核空间。

Binder 就是一种把这四个组件粘合在一起的粘结剂。

这个流程是如何进行的呢?

借用 Android Binder机制(一) Binder的设计和框架 的图片:

上图简单介绍(节选自 http://wangkuiwu.github.io/2014/09/01/Binder-Introduce/):

  • Binder实体

    • Binder 实体实际上是内核中 binder_node 结构体的对象,它的作用是在内核中保存 Server 和ServiceManager 的信息(例如,Binder 实体中保存了 Server 对象在用户空间的地址)
    • Binder 实体是 Server 在 Binder 驱动中的存在形式,内核通过 Binder 实体可以找到用户空间的Server对象
    • 在上图中,Server 和 ServiceManager 在 Binder 驱动中都对应的存在一个 Binder 实体
  • Binder 引用
    • Binder引用实际上是内核中 binder_ref 结构体的对象,是某一个 Binder 实体的引用,通过Binder 引用可以在内核中找到对应的 Binder 实体
    • 如果将 Server 看作是 Binder 实体的话,那么 Client 就好比 Binder 引用,Client 通过保存一个Server 对象的 Binder 引用,再通过该 Binder 引用在内核中找到对应的 Binder 实体,进而找到Server 对象,然后将通信内容发送给 Server 对象
  • 远程服务
    • 本地服务的代理,通过调用远程服务可以间接调用本地服务

1.Client、Server 和 ServiceManager 处于用户空间的不同进程。

2.Binder 实体和 Binder 引用都是内核(即 Binder 驱动)中的数据结构。

它们的关系如下:

每一个 Server 在内核中就表现为一个 Binder 实体,而每一个 Client 则表现为一个 Binder 引用。这样,每个 Binder 引用都对应一个 Binder 实体,而每个 Binder 实体则可以多个 Binder 引用。

Binder 跨进程通讯流程主要为如下 4 步:

  1. ServiceManager 初始化

    • 当该应用程序启动时,ServiceManager 会和 Binder 驱动进行通信,告诉 Binder 驱动它是服务管理者
    • Binder 驱动新建 ServiceManager 对应的 Binder 实体
  2. Server 向 ServiceManager 注册自己
    • Server 向 Binder 驱动发起注册请求,Binder 为它创建 Binder 实体
    • 然后如果 ServiceManager 中没有这个 Server 时就添加 Server 名称与 Binder 引用到它的 Binder 引用表
  3. Client 获取远程服务
    • Client 首先会向 Binder 驱动发起获取服务的请求,传递要获取的服务名称
    • Binder 驱动将该请求转发给 ServiceManager 进程
    • ServiceManager 查找到 Client 需要的 Server 对应的 Binder 实体的 Binder 引用信息,然后通过 Binder 驱动反馈给 Client
    • Client 收到 Server 对应的 Binder 引用后,会创建一个 Server 对应的远程服务(即 Server 在当前进程的代理)
  4. Client 通过代理调用 Server
    • Client 调用远程服务,远程服务收到 Client 请求之后,会和 Binder 驱动通信
    • 因为远程服务中有 Server 的 Binder 引用信息,因此驱动就能轻易的找到对应的 Server,进而将Client 的请求内容发送 Server

Binder 机制的优点

对比 Linux 上的其他进程通信方式(管道/消息队列/共享内存/信号量/Socket),Binder 机制的优点有以下几点:

  1. 高效简单

    • 通过驱动在内核空间拷贝数据,不需要额外的同步处理
    • 对比 Socket 等传输效率高
  2. 安全

    • Binder 机制为每个进程分配了 UID/PID 来作为鉴别身份的标示,并且在 Binder 通信时会根据UID/PID 进行有效性检测
  3. Client/Server 架构

    • 这种架构使得通讯更为简单

总结

Binder 机制的学习过程是痛苦的 T.T,即使光看 Java 层也费了不少功夫,这可能是由于我实战中没有遇到,光看源码和抽象概念掌握太慢的原因吧。

本文简单介绍了 Binder 机制,参考了很多优秀的文章,真佩服他们!

借用《Android 开发艺术探索》对 Binder 的概括:

  • 从代码角度来看,Binder 是一个类,实现了 IBinder 接口;
  • 从来源看,Binder 来自于 OpenBinder,是 Android IPC 机制中的一种,Binder 还可以理解成一个虚拟物理设备,设备驱动是dev/binder;
  • 从 Framework 层看,Binder 是 Service Manager 连接各种Manager(ActivityManager,PackageManager…) 和相应Service (ActivityManagerService, PackageManagerService…) 的桥梁;
  • 从客户端看,Binder 是客户端服务器通讯的媒介

在对 Binder 有了一定理解后,下篇文章我们就去看看 AIDL 帮我们做了什么。

Thanks

《Android 开发艺术探索》

https://developer.android.com/reference/android/os/IBinder.html

http://blog.csdn.net/luoshengyang/article/details/6618363

http://blog.csdn.net/luoshengyang/article/details/6642463

http://weishu.me/2016/01/12/binder-index-for-newer/

http://wangkuiwu.github.io/2014/09/01/Binder-Introduce/

http://blog.csdn.net/u010132993/article/details/72582655

Android 进阶8:进程通信之 Binder 机制浅析的更多相关文章

  1. AIDL/IPC Android AIDL/IPC 进程通信机制——超具体解说及使用方法案例剖析(播放器)

    首先引申下AIDL.什么是AIDL呢?IPC? ------ Designing a Remote Interface Using AIDL 通常情况下,我们在同一进程内会使用Binder.Broad ...

  2. 从Android源码的角度分析Binder机制

    欢迎访问我的个人博客,原文链接:http://wensibo.top/2017/07/03/Binder/ ,未经允许不得转载! 前言 大家好,好久不见,距离上篇文章已经有35天之久了,因为身体不舒服 ...

  3. ANDROID BINDER机制浅析

    Binder是Android上一种IPC机制,重要且较难理解.由于Linux上标准IPC在灵活和可靠性存在一定不足,Google基于OpenBinder的设计和构想实现了Binder. 本文只简单介绍 ...

  4. Android上的进程通信(IPC)机制

    Interprocess Communication Android offers a mechanism for interprocess communication (IPC) using rem ...

  5. Android 进阶13:几种进程通信方式的对比总结

    不花时间打基础,你将会花更多时间解决那些不必要的问题. 读完本文你将了解: RPC 是什么 IDL 是什么 IPC 是什么 Android 几种进程通信方式 如何选择这几种通信方式 Thanks RP ...

  6. Android 进阶9:进程通信之 AIDL 解析

    读完本文你将了解: AIDL AIDL 生成文件分析 Stub Proxy AIDL 生成的内容小结 AIDL 的使用回顾 服务端 客户端 小结 手动写个 Binder 首先是定义跨进程接口实现 II ...

  7. 图文详解 Android Binder跨进程通信机制 原理

    图文详解 Android Binder跨进程通信机制 原理 目录 目录 1. Binder到底是什么? 中文即 粘合剂,意思为粘合了两个不同的进程 网上有很多对Binder的定义,但都说不清楚:Bin ...

  8. 【朝花夕拾】Android性能篇之(七)Android跨进程通信篇

    前言 只要是面试高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点之一.Android系统的运行由大量相互独立的进程相互协助来完成的,所以Android进程间通信问题,是做好Andro ...

  9. Android : 跟我学Binder --- (1) 什么是Binder IPC?为何要使用Binder机制?

    目录: Android : 跟我学Binder --- (1) 什么是Binder IPC?为何要使用Binder机制? Android : 跟我学Binder --- (2) AIDL分析及手动实现 ...

随机推荐

  1. 解释一下python中的逻辑运算符

    python中有三个逻辑运算符:and.or.not print(False and True)#False print(7<7 or True)#True print(not 2==2)#Fa ...

  2. 动态切换数据库(EF框架)

             文章简略:本文测试项目为Silverlight+EF+RIA Service动态切换数据库的问题 通常,Ado.net EntityFramework的数据库连接字符串Connect ...

  3. linux 查看tomcat 日志

    tomcat 重启: cd /opt/appserver/apache-tomcat-/bin ./shutdown.sh -ef|grep tomcat kill - ./startup.sh 查看 ...

  4. CodeForces - 920F SUM and REPLACE (线段树)

    题意:给N个数M次操作,(1<=N,M<=3e5, 1<=ai<=1e6),1是使[L,R]中的每个元素变成其因子的个数之和:2是求[L,R]区间之和 分析:看上去就很线段树的 ...

  5. Linux信号signal处理机制

    信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断.从它的命名可以看出,它的实质和使用很象中断.所以,信号可以说是进程控制的一部分.         一.信号的基本概念 ...

  6. npm国内镜像设置

    http://cnodejs.org/topic/4f9904f9407edba21468f31e

  7. MySQL-5.7密码策略及用户资源限制

    1.密码策略 在mysql 5.6对密码的强度进行了加强,推出了validate_password 插件.支持密码的强度要求. (1)安装插件 [root@localhost ~]# ll /usr/ ...

  8. Microsoft.VisualStudio.Web.PageInspector.Loader

    未能加载文件或程序集"Microsoft.VisualStudio.Web.PageInspector.Loader, Version=1.0.0.0, Culture=neutral, P ...

  9. H5新特性---新应用

    1.持久化本地存储 可以不通过第三方插件实现数据的本地存储 2.WebSocket 页面之间可以双向通信 3.服务器推送事件(SSE) 从Web服务器将消息推送给浏览器(在手机中常见) 例如: < ...

  10. [pixhawk笔记]4-如何写一个简单的应用程序

    本文主要内容来自于:https://dev.px4.io/en/tutorials/tutorial_hello_sky.html,并对文档中的部分问题进行更正. 本文假设已经建立好开发环境并能正确编 ...